@nowline/renderer 0.5.0 → 0.6.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/svg/render.d.ts +18 -4
- package/dist/svg/render.d.ts.map +1 -1
- package/dist/svg/render.js +66 -62
- package/dist/svg/render.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/svg/render.ts +101 -56
package/src/svg/render.ts
CHANGED
|
@@ -95,6 +95,16 @@ export interface AssetBytes {
|
|
|
95
95
|
|
|
96
96
|
export type AssetResolver = (ref: string) => Promise<AssetBytes>;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Per-role `font-family` strings the renderer stamps onto `<text>` elements.
|
|
100
|
+
* Defaults to the shared, portable `FONT_STACK` (generic CSS stacks). Raster
|
|
101
|
+
* and preview callers override this with a *pinned* family (e.g. the bundled
|
|
102
|
+
* `DejaVu Sans` / `DejaVu Sans Mono`) so the rendered SVG names exactly the
|
|
103
|
+
* font that resvg / the webview `@font-face` actually provide — the WYSIWYG
|
|
104
|
+
* contract. The `.svg` file export keeps the default portable stack.
|
|
105
|
+
*/
|
|
106
|
+
export type FontFamilies = Record<'sans' | 'serif' | 'mono', string>;
|
|
107
|
+
|
|
98
108
|
export interface RenderOptions {
|
|
99
109
|
assetResolver?: AssetResolver;
|
|
100
110
|
noLinks?: boolean;
|
|
@@ -102,6 +112,11 @@ export interface RenderOptions {
|
|
|
102
112
|
warn?: (message: string) => void;
|
|
103
113
|
// Override the deterministic id prefix (defaults to 'nl').
|
|
104
114
|
idPrefix?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Override per-role `font-family` strings. Defaults to the portable
|
|
117
|
+
* `FONT_STACK`. Set to a pinned family for raster/preview WYSIWYG.
|
|
118
|
+
*/
|
|
119
|
+
fontFamilies?: FontFamilies;
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
// `TEXT_SIZE_PX`, `CORNER_RADIUS_PX`, `FONT_STACK` come from
|
|
@@ -125,9 +140,13 @@ function textSizePx(bucket: ResolvedStyle['textSize']): number {
|
|
|
125
140
|
return (TEXT_SIZE_PX as Record<string, number>)[bucket] ?? 14;
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
function fontAttrs(
|
|
143
|
+
function fontAttrs(
|
|
144
|
+
style: ResolvedStyle,
|
|
145
|
+
fonts: FontFamilies,
|
|
146
|
+
overrideSize?: number,
|
|
147
|
+
): Record<string, string | number> {
|
|
129
148
|
return {
|
|
130
|
-
'font-family':
|
|
149
|
+
'font-family': fonts[style.font],
|
|
131
150
|
'font-size': overrideSize ?? textSizePx(style.textSize),
|
|
132
151
|
'font-weight': WEIGHT_NUM[style.weight] ?? 400,
|
|
133
152
|
'font-style': style.italic ? 'italic' : 'normal',
|
|
@@ -452,7 +471,12 @@ function rectFrame(
|
|
|
452
471
|
});
|
|
453
472
|
}
|
|
454
473
|
|
|
455
|
-
function renderHeader(
|
|
474
|
+
function renderHeader(
|
|
475
|
+
h: PositionedHeader,
|
|
476
|
+
idPrefix: string,
|
|
477
|
+
palette: Theme,
|
|
478
|
+
fonts: FontFamilies,
|
|
479
|
+
): string {
|
|
456
480
|
// The layout has already sized the card to its (wrapped) text content
|
|
457
481
|
// and stashed the bounds in `h.cardBox`, with `h.titleLines` /
|
|
458
482
|
// `h.authorLines` ready to render line-by-line. See sizeBesideHeader
|
|
@@ -485,7 +509,7 @@ function renderHeader(h: PositionedHeader, idPrefix: string, palette: Theme): st
|
|
|
485
509
|
{
|
|
486
510
|
x: num(cardX + HEADER_CARD_PADDING_X),
|
|
487
511
|
y: num(cardY + HEADER_CARD_PADDING_TOP + i * HEADER_TITLE_LINE_HEIGHT_PX),
|
|
488
|
-
'font-family':
|
|
512
|
+
'font-family': fonts[h.style.font],
|
|
489
513
|
'font-size': HEADER_TITLE_FONT_SIZE_PX,
|
|
490
514
|
'font-weight': 600,
|
|
491
515
|
fill: h.style.text,
|
|
@@ -511,7 +535,7 @@ function renderHeader(h: PositionedHeader, idPrefix: string, palette: Theme): st
|
|
|
511
535
|
HEADER_TITLE_TO_AUTHOR_GAP_PX +
|
|
512
536
|
j * HEADER_AUTHOR_LINE_HEIGHT_PX,
|
|
513
537
|
),
|
|
514
|
-
'font-family':
|
|
538
|
+
'font-family': fonts[h.style.font],
|
|
515
539
|
'font-size': HEADER_AUTHOR_FONT_SIZE_PX,
|
|
516
540
|
fill: authorColor,
|
|
517
541
|
},
|
|
@@ -602,7 +626,7 @@ function renderGridLines(t: PositionedTimelineScale, swimlaneTopY: number, palet
|
|
|
602
626
|
return tag('g', { 'data-layer': 'grid' }, parts.join(''));
|
|
603
627
|
}
|
|
604
628
|
|
|
605
|
-
function renderTimeline(t: PositionedTimelineScale, palette: Theme): string {
|
|
629
|
+
function renderTimeline(t: PositionedTimelineScale, palette: Theme, fonts: FontFamilies): string {
|
|
606
630
|
const panelFill = palette.timeline.panelFill;
|
|
607
631
|
const borderColor = palette.timeline.border;
|
|
608
632
|
const labelColor = palette.timeline.labelText;
|
|
@@ -683,7 +707,7 @@ function renderTimeline(t: PositionedTimelineScale, palette: Theme): string {
|
|
|
683
707
|
{
|
|
684
708
|
x: num(tick.labelX),
|
|
685
709
|
y: num(tickPanelY + TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX),
|
|
686
|
-
'font-family':
|
|
710
|
+
'font-family': fonts.sans,
|
|
687
711
|
'font-size': 10,
|
|
688
712
|
fill: labelColor,
|
|
689
713
|
'text-anchor': 'middle',
|
|
@@ -698,7 +722,7 @@ function renderTimeline(t: PositionedTimelineScale, palette: Theme): string {
|
|
|
698
722
|
{
|
|
699
723
|
x: num(tick.labelX),
|
|
700
724
|
y: num(bottomTickPanelY! + TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX),
|
|
701
|
-
'font-family':
|
|
725
|
+
'font-family': fonts.sans,
|
|
702
726
|
'font-size': 10,
|
|
703
727
|
fill: labelColor,
|
|
704
728
|
'text-anchor': 'middle',
|
|
@@ -711,7 +735,7 @@ function renderTimeline(t: PositionedTimelineScale, palette: Theme): string {
|
|
|
711
735
|
return tag('g', { 'data-layer': 'timeline' }, parts.join(''));
|
|
712
736
|
}
|
|
713
737
|
|
|
714
|
-
function renderNowline(n: PositionedNowline | null, palette: Theme): string {
|
|
738
|
+
function renderNowline(n: PositionedNowline | null, palette: Theme, fonts: FontFamilies): string {
|
|
715
739
|
if (!n) return '';
|
|
716
740
|
const color = palette.nowline.stroke;
|
|
717
741
|
const labelTextColor = palette.nowline.labelText;
|
|
@@ -735,7 +759,7 @@ function renderNowline(n: PositionedNowline | null, palette: Theme): string {
|
|
|
735
759
|
// The squared edge IS the line; the rounded edge points into the
|
|
736
760
|
// chart, so the pill always hugs the line and never overflows.
|
|
737
761
|
const pillBg = renderNowPillBg(n, color);
|
|
738
|
-
const label = renderNowPillLabel(n, labelTextColor);
|
|
762
|
+
const label = renderNowPillLabel(n, labelTextColor, fonts);
|
|
739
763
|
return tag('g', { 'data-layer': 'nowline' }, line + pillBg + label);
|
|
740
764
|
}
|
|
741
765
|
|
|
@@ -812,7 +836,11 @@ function renderNowPillBg(n: PositionedNowline, color: string): string {
|
|
|
812
836
|
return tag('path', { d, fill: color });
|
|
813
837
|
}
|
|
814
838
|
|
|
815
|
-
function renderNowPillLabel(
|
|
839
|
+
function renderNowPillLabel(
|
|
840
|
+
n: PositionedNowline,
|
|
841
|
+
labelTextColor: string,
|
|
842
|
+
fonts: FontFamilies,
|
|
843
|
+
): string {
|
|
816
844
|
const baselineY = n.pillTopY + NOW_PILL_LABEL_BASELINE_OFFSET_PX;
|
|
817
845
|
const edgeX = squaredEdgeX(n);
|
|
818
846
|
let labelX: number;
|
|
@@ -831,7 +859,7 @@ function renderNowPillLabel(n: PositionedNowline, labelTextColor: string): strin
|
|
|
831
859
|
{
|
|
832
860
|
x: num(labelX),
|
|
833
861
|
y: num(baselineY),
|
|
834
|
-
'font-family':
|
|
862
|
+
'font-family': fonts.sans,
|
|
835
863
|
'font-size': NOW_PILL_LABEL_FONT_SIZE_PX,
|
|
836
864
|
'font-weight': 700,
|
|
837
865
|
fill: labelTextColor,
|
|
@@ -846,6 +874,7 @@ function renderItem(
|
|
|
846
874
|
options: RenderOptions,
|
|
847
875
|
idPrefix: string,
|
|
848
876
|
palette: Theme,
|
|
877
|
+
fonts: FontFamilies,
|
|
849
878
|
): string {
|
|
850
879
|
const parts: string[] = [];
|
|
851
880
|
const shadow = shadowFilterUrl(idPrefix, i.style.shadow);
|
|
@@ -949,7 +978,7 @@ function renderItem(
|
|
|
949
978
|
{
|
|
950
979
|
x: num(captionX),
|
|
951
980
|
y: num(i.box.y + ITEM_CAPTION_TITLE_BASELINE_OFFSET_PX),
|
|
952
|
-
'font-family':
|
|
981
|
+
'font-family': fonts[i.style.font],
|
|
953
982
|
'font-size': ITEM_CAPTION_TITLE_FONT_SIZE_PX,
|
|
954
983
|
'font-weight': 600,
|
|
955
984
|
fill: titleColor,
|
|
@@ -973,7 +1002,7 @@ function renderItem(
|
|
|
973
1002
|
x: captionX,
|
|
974
1003
|
baselineY: i.box.y + ITEM_CAPTION_META_BASELINE_OFFSET_PX,
|
|
975
1004
|
fontSize: ITEM_CAPTION_META_FONT_SIZE_PX,
|
|
976
|
-
fontFamily:
|
|
1005
|
+
fontFamily: fonts[i.style.font],
|
|
977
1006
|
color: metaColor,
|
|
978
1007
|
}),
|
|
979
1008
|
);
|
|
@@ -1004,7 +1033,7 @@ function renderItem(
|
|
|
1004
1033
|
{
|
|
1005
1034
|
x: num(fx),
|
|
1006
1035
|
y: num(footnoteY),
|
|
1007
|
-
'font-family':
|
|
1036
|
+
'font-family': fonts.sans,
|
|
1008
1037
|
'font-size': 10,
|
|
1009
1038
|
'font-weight': 700,
|
|
1010
1039
|
fill: captionOutsideTextColor,
|
|
@@ -1023,7 +1052,7 @@ function renderItem(
|
|
|
1023
1052
|
{
|
|
1024
1053
|
x: num(fx),
|
|
1025
1054
|
y: num(footnoteY),
|
|
1026
|
-
'font-family':
|
|
1055
|
+
'font-family': fonts.sans,
|
|
1027
1056
|
'font-size': 10,
|
|
1028
1057
|
'font-weight': 700,
|
|
1029
1058
|
fill: i.style.text,
|
|
@@ -1108,7 +1137,7 @@ function renderItem(
|
|
|
1108
1137
|
{
|
|
1109
1138
|
x: num(i.overflowBox.x + i.overflowBox.width / 2),
|
|
1110
1139
|
y: num(i.overflowBox.y + i.overflowBox.height / 2 + 3),
|
|
1111
|
-
'font-family':
|
|
1140
|
+
'font-family': fonts.sans,
|
|
1112
1141
|
'font-size': 9,
|
|
1113
1142
|
'font-weight': 700,
|
|
1114
1143
|
fill: captionColor,
|
|
@@ -1140,7 +1169,7 @@ function renderItem(
|
|
|
1140
1169
|
{
|
|
1141
1170
|
x: num(chip.box.x + chip.box.width / 2),
|
|
1142
1171
|
y: num(chip.box.y + chip.box.height / 2 + 3),
|
|
1143
|
-
...fontAttrs(chip.style, TEXT_SIZE_PX.xs),
|
|
1172
|
+
...fontAttrs(chip.style, fonts, TEXT_SIZE_PX.xs),
|
|
1144
1173
|
'text-anchor': 'middle',
|
|
1145
1174
|
},
|
|
1146
1175
|
chip.text,
|
|
@@ -1155,6 +1184,7 @@ function renderGroup(
|
|
|
1155
1184
|
options: RenderOptions,
|
|
1156
1185
|
idPrefix: string,
|
|
1157
1186
|
palette: Theme,
|
|
1187
|
+
fonts: FontFamilies,
|
|
1158
1188
|
): string {
|
|
1159
1189
|
const parts: string[] = [];
|
|
1160
1190
|
const hasFill = g.style.bg !== 'none' && g.style.bg !== '#ffffff';
|
|
@@ -1211,7 +1241,7 @@ function renderGroup(
|
|
|
1211
1241
|
{
|
|
1212
1242
|
x: num(tabX + GROUP_TITLE_TAB_PAD_X_PX),
|
|
1213
1243
|
y: num(tabY + GROUP_TITLE_TAB_LABEL_BASELINE_OFFSET_PX),
|
|
1214
|
-
'font-family':
|
|
1244
|
+
'font-family': fonts[g.style.font],
|
|
1215
1245
|
'font-size': GROUP_TITLE_TAB_LABEL_FONT_SIZE_PX,
|
|
1216
1246
|
'font-weight': 600,
|
|
1217
1247
|
fill: '#ffffff',
|
|
@@ -1259,7 +1289,7 @@ function renderGroup(
|
|
|
1259
1289
|
{
|
|
1260
1290
|
x: num(g.box.x + 6),
|
|
1261
1291
|
y: num(g.box.y - 2),
|
|
1262
|
-
...fontAttrs(g.style, TEXT_SIZE_PX.xs),
|
|
1292
|
+
...fontAttrs(g.style, fonts, TEXT_SIZE_PX.xs),
|
|
1263
1293
|
'fill-opacity': 0.7,
|
|
1264
1294
|
},
|
|
1265
1295
|
g.title,
|
|
@@ -1273,7 +1303,7 @@ function renderGroup(
|
|
|
1273
1303
|
// bounding box.
|
|
1274
1304
|
parts.push(renderInlineDatePins(g.inlineDatePins, g.style.fg));
|
|
1275
1305
|
for (const c of g.children) {
|
|
1276
|
-
parts.push(renderTrackChild(c, options, idPrefix, palette));
|
|
1306
|
+
parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
|
|
1277
1307
|
}
|
|
1278
1308
|
void palette;
|
|
1279
1309
|
return tag('g', { 'data-layer': 'group', 'data-id': g.id ?? null }, parts.join(''));
|
|
@@ -1284,6 +1314,7 @@ function renderParallel(
|
|
|
1284
1314
|
options: RenderOptions,
|
|
1285
1315
|
idPrefix: string,
|
|
1286
1316
|
palette: Theme,
|
|
1317
|
+
fonts: FontFamilies,
|
|
1287
1318
|
): string {
|
|
1288
1319
|
const parts: string[] = [];
|
|
1289
1320
|
// `bracket: solid|dashed` parallels render explicit [ ] brackets framing
|
|
@@ -1323,7 +1354,7 @@ function renderParallel(
|
|
|
1323
1354
|
{
|
|
1324
1355
|
x: num(p.box.x + 4),
|
|
1325
1356
|
y: num(p.box.y - 2),
|
|
1326
|
-
...fontAttrs(p.style, TEXT_SIZE_PX.xs),
|
|
1357
|
+
...fontAttrs(p.style, fonts, TEXT_SIZE_PX.xs),
|
|
1327
1358
|
'fill-opacity': 0.7,
|
|
1328
1359
|
},
|
|
1329
1360
|
p.title,
|
|
@@ -1334,7 +1365,7 @@ function renderParallel(
|
|
|
1334
1365
|
// `before:DATE`). Painted before children so child bars sit on top.
|
|
1335
1366
|
parts.push(renderInlineDatePins(p.inlineDatePins, p.style.fg));
|
|
1336
1367
|
for (const c of p.children) {
|
|
1337
|
-
parts.push(renderTrackChild(c, options, idPrefix, palette));
|
|
1368
|
+
parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
|
|
1338
1369
|
}
|
|
1339
1370
|
return tag('g', { 'data-layer': 'parallel', 'data-id': p.id ?? null }, parts.join(''));
|
|
1340
1371
|
}
|
|
@@ -1344,10 +1375,11 @@ function renderTrackChild(
|
|
|
1344
1375
|
options: RenderOptions,
|
|
1345
1376
|
idPrefix: string,
|
|
1346
1377
|
palette: Theme,
|
|
1378
|
+
fonts: FontFamilies,
|
|
1347
1379
|
): string {
|
|
1348
|
-
if (c.kind === 'item') return renderItem(c, options, idPrefix, palette);
|
|
1349
|
-
if (c.kind === 'group') return renderGroup(c, options, idPrefix, palette);
|
|
1350
|
-
return renderParallel(c, options, idPrefix, palette);
|
|
1380
|
+
if (c.kind === 'item') return renderItem(c, options, idPrefix, palette, fonts);
|
|
1381
|
+
if (c.kind === 'group') return renderGroup(c, options, idPrefix, palette, fonts);
|
|
1382
|
+
return renderParallel(c, options, idPrefix, palette, fonts);
|
|
1351
1383
|
}
|
|
1352
1384
|
|
|
1353
1385
|
// Renders only the swimlane's background tint rect. Emitted before the
|
|
@@ -1425,6 +1457,7 @@ function renderSwimlane(
|
|
|
1425
1457
|
options: RenderOptions,
|
|
1426
1458
|
idPrefix: string,
|
|
1427
1459
|
palette: Theme,
|
|
1460
|
+
fonts: FontFamilies,
|
|
1428
1461
|
): string {
|
|
1429
1462
|
const tabFill = palette.swimlane.tabFill;
|
|
1430
1463
|
const tabStroke = palette.swimlane.tabStroke;
|
|
@@ -1481,7 +1514,7 @@ function renderSwimlane(
|
|
|
1481
1514
|
{
|
|
1482
1515
|
x: num(tab.titleX),
|
|
1483
1516
|
y: num(labelY),
|
|
1484
|
-
'font-family':
|
|
1517
|
+
'font-family': fonts[s.style.font],
|
|
1485
1518
|
'font-size': 12,
|
|
1486
1519
|
'font-weight': 600,
|
|
1487
1520
|
fill: tabText,
|
|
@@ -1495,7 +1528,7 @@ function renderSwimlane(
|
|
|
1495
1528
|
{
|
|
1496
1529
|
x: num(tab.ownerX),
|
|
1497
1530
|
y: num(labelY),
|
|
1498
|
-
'font-family':
|
|
1531
|
+
'font-family': fonts[s.style.font],
|
|
1499
1532
|
'font-size': 10,
|
|
1500
1533
|
fill: ownerText,
|
|
1501
1534
|
},
|
|
@@ -1515,7 +1548,7 @@ function renderSwimlane(
|
|
|
1515
1548
|
tab.badgeX,
|
|
1516
1549
|
labelY,
|
|
1517
1550
|
LANE_BADGE_FONT_SIZE_PX,
|
|
1518
|
-
|
|
1551
|
+
fonts[s.style.font],
|
|
1519
1552
|
ownerText,
|
|
1520
1553
|
),
|
|
1521
1554
|
);
|
|
@@ -1526,7 +1559,7 @@ function renderSwimlane(
|
|
|
1526
1559
|
{
|
|
1527
1560
|
x: num(tab.footnoteRightX),
|
|
1528
1561
|
y: num(tabY + 14),
|
|
1529
|
-
'font-family':
|
|
1562
|
+
'font-family': fonts.sans,
|
|
1530
1563
|
'font-size': LANE_BADGE_FONT_SIZE_PX,
|
|
1531
1564
|
'font-weight': 700,
|
|
1532
1565
|
fill: footnoteColor,
|
|
@@ -1538,7 +1571,7 @@ function renderSwimlane(
|
|
|
1538
1571
|
}
|
|
1539
1572
|
}
|
|
1540
1573
|
for (const c of s.children) {
|
|
1541
|
-
parts.push(renderTrackChild(c, options, idPrefix, palette));
|
|
1574
|
+
parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
|
|
1542
1575
|
}
|
|
1543
1576
|
// m13: tri-state utilization underline along the band's bottom edge.
|
|
1544
1577
|
// Painted after items so it overlays any item that happens to extend
|
|
@@ -1548,7 +1581,7 @@ function renderSwimlane(
|
|
|
1548
1581
|
return tag('g', { 'data-layer': 'swimlane', 'data-id': s.id ?? null }, parts.join(''));
|
|
1549
1582
|
}
|
|
1550
1583
|
|
|
1551
|
-
function renderAnchor(a: PositionedAnchor, palette: Theme): string {
|
|
1584
|
+
function renderAnchor(a: PositionedAnchor, palette: Theme, fonts: FontFamilies): string {
|
|
1552
1585
|
const size = a.radius;
|
|
1553
1586
|
const cx = a.center.x;
|
|
1554
1587
|
const cy = a.center.y;
|
|
@@ -1573,7 +1606,7 @@ function renderAnchor(a: PositionedAnchor, palette: Theme): string {
|
|
|
1573
1606
|
const labelAttrs: Record<string, string | number | null | undefined> = {
|
|
1574
1607
|
x: num(labelX),
|
|
1575
1608
|
y: num(cy + 4),
|
|
1576
|
-
'font-family':
|
|
1609
|
+
'font-family': fonts.sans,
|
|
1577
1610
|
'font-size': 10,
|
|
1578
1611
|
fill: labelColor,
|
|
1579
1612
|
};
|
|
@@ -1595,7 +1628,7 @@ function renderAnchorCutLine(a: PositionedAnchor, palette: Theme): string {
|
|
|
1595
1628
|
});
|
|
1596
1629
|
}
|
|
1597
1630
|
|
|
1598
|
-
function renderMilestone(m: PositionedMilestone, palette: Theme): string {
|
|
1631
|
+
function renderMilestone(m: PositionedMilestone, palette: Theme, fonts: FontFamilies): string {
|
|
1599
1632
|
const cx = m.center.x;
|
|
1600
1633
|
const cy = m.center.y;
|
|
1601
1634
|
const r = m.radius;
|
|
@@ -1614,7 +1647,7 @@ function renderMilestone(m: PositionedMilestone, palette: Theme): string {
|
|
|
1614
1647
|
const labelAttrs: Record<string, string | number | null | undefined> = {
|
|
1615
1648
|
x: num(labelX),
|
|
1616
1649
|
y: num(cy + 4),
|
|
1617
|
-
'font-family':
|
|
1650
|
+
'font-family': fonts.sans,
|
|
1618
1651
|
'font-size': 10,
|
|
1619
1652
|
'font-weight': 600,
|
|
1620
1653
|
fill: labelColor,
|
|
@@ -1715,7 +1748,12 @@ function roundedOrthogonalPath(points: Point[], radius: number): string {
|
|
|
1715
1748
|
return parts.join(' ');
|
|
1716
1749
|
}
|
|
1717
1750
|
|
|
1718
|
-
function renderFootnotes(
|
|
1751
|
+
function renderFootnotes(
|
|
1752
|
+
f: PositionedFootnoteArea,
|
|
1753
|
+
idPrefix: string,
|
|
1754
|
+
palette: Theme,
|
|
1755
|
+
fonts: FontFamilies,
|
|
1756
|
+
): string {
|
|
1719
1757
|
if (f.entries.length === 0) return '';
|
|
1720
1758
|
const panelFill = palette.footnotePanel.fill;
|
|
1721
1759
|
const borderColor = palette.footnotePanel.border;
|
|
@@ -1743,7 +1781,7 @@ function renderFootnotes(f: PositionedFootnoteArea, idPrefix: string, palette: T
|
|
|
1743
1781
|
{
|
|
1744
1782
|
x: num(f.box.x + FOOTNOTE_PANEL_PADDING_PX),
|
|
1745
1783
|
y: num(f.box.y + FOOTNOTE_HEADER_BASELINE_OFFSET_PX),
|
|
1746
|
-
'font-family':
|
|
1784
|
+
'font-family': fonts.sans,
|
|
1747
1785
|
'font-size': 12,
|
|
1748
1786
|
'font-weight': 700,
|
|
1749
1787
|
fill: headerColor,
|
|
@@ -1763,7 +1801,7 @@ function renderFootnotes(f: PositionedFootnoteArea, idPrefix: string, palette: T
|
|
|
1763
1801
|
{
|
|
1764
1802
|
x: num(numberX),
|
|
1765
1803
|
y: num(y),
|
|
1766
|
-
'font-family':
|
|
1804
|
+
'font-family': fonts.sans,
|
|
1767
1805
|
'font-size': 10,
|
|
1768
1806
|
'font-weight': 700,
|
|
1769
1807
|
fill: numberColor,
|
|
@@ -1776,7 +1814,7 @@ function renderFootnotes(f: PositionedFootnoteArea, idPrefix: string, palette: T
|
|
|
1776
1814
|
{
|
|
1777
1815
|
x: num(titleX),
|
|
1778
1816
|
y: num(y),
|
|
1779
|
-
'font-family':
|
|
1817
|
+
'font-family': fonts.sans,
|
|
1780
1818
|
'font-size': 11,
|
|
1781
1819
|
'font-weight': 600,
|
|
1782
1820
|
fill: titleColor,
|
|
@@ -1790,7 +1828,7 @@ function renderFootnotes(f: PositionedFootnoteArea, idPrefix: string, palette: T
|
|
|
1790
1828
|
{
|
|
1791
1829
|
x: num(titleX + Math.max(120, e.title.length * 6)),
|
|
1792
1830
|
y: num(y),
|
|
1793
|
-
'font-family':
|
|
1831
|
+
'font-family': fonts.sans,
|
|
1794
1832
|
'font-size': 11,
|
|
1795
1833
|
fill: descColor,
|
|
1796
1834
|
},
|
|
@@ -1807,6 +1845,7 @@ function renderIncludeRegion(
|
|
|
1807
1845
|
options: RenderOptions,
|
|
1808
1846
|
idPrefix: string,
|
|
1809
1847
|
palette: Theme,
|
|
1848
|
+
fonts: FontFamilies,
|
|
1810
1849
|
): string {
|
|
1811
1850
|
const border = palette.includeRegion.border;
|
|
1812
1851
|
const fill = palette.includeRegion.fill;
|
|
@@ -1857,7 +1896,7 @@ function renderIncludeRegion(
|
|
|
1857
1896
|
{
|
|
1858
1897
|
x: num(chrome.tabLabelX),
|
|
1859
1898
|
y: num(tabY + FRAME_TAB_LABEL_BASELINE_OFFSET_PX),
|
|
1860
|
-
'font-family':
|
|
1899
|
+
'font-family': fonts.sans,
|
|
1861
1900
|
'font-size': 11,
|
|
1862
1901
|
'font-weight': 600,
|
|
1863
1902
|
fill: tabText,
|
|
@@ -1915,7 +1954,7 @@ function renderIncludeRegion(
|
|
|
1915
1954
|
{
|
|
1916
1955
|
x: num(chrome.sourceTextX),
|
|
1917
1956
|
y: num(sourceTextY),
|
|
1918
|
-
'font-family':
|
|
1957
|
+
'font-family': fonts.mono,
|
|
1919
1958
|
'font-size': sourceFontSize,
|
|
1920
1959
|
fill: badgeText,
|
|
1921
1960
|
},
|
|
@@ -1924,7 +1963,7 @@ function renderIncludeRegion(
|
|
|
1924
1963
|
|
|
1925
1964
|
// Nested swimlanes (laid out by buildIncludeRegions against the parent's timeline).
|
|
1926
1965
|
const nested = r.nestedSwimlanes
|
|
1927
|
-
.map((s) => renderSwimlane(s, options, idPrefix, palette))
|
|
1966
|
+
.map((s) => renderSwimlane(s, options, idPrefix, palette, fonts))
|
|
1928
1967
|
.join('');
|
|
1929
1968
|
|
|
1930
1969
|
return tag(
|
|
@@ -1940,7 +1979,7 @@ function renderIncludeRegion(
|
|
|
1940
1979
|
// entire string is clickable. Glyph anatomy (positions, widths, scale)
|
|
1941
1980
|
// lives in `themes/shared.ts` (`ATTRIBUTION_*`); the layout reserves a
|
|
1942
1981
|
// box of exactly that size at canvas-bottom-right.
|
|
1943
|
-
function renderAttributionMark(model: PositionedRoadmap): string {
|
|
1982
|
+
function renderAttributionMark(model: PositionedRoadmap, fonts: FontFamilies): string {
|
|
1944
1983
|
const muted = model.palette.attribution.mark;
|
|
1945
1984
|
const accent = model.palette.attribution.link;
|
|
1946
1985
|
if (model.swimlanes.length === 0) return '';
|
|
@@ -1955,7 +1994,7 @@ function renderAttributionMark(model: PositionedRoadmap): string {
|
|
|
1955
1994
|
{
|
|
1956
1995
|
x: '0',
|
|
1957
1996
|
y: baselineY,
|
|
1958
|
-
'font-family':
|
|
1997
|
+
'font-family': fonts.sans,
|
|
1959
1998
|
'font-size': ATTRIBUTION_PREFIX_FONT_SIZE,
|
|
1960
1999
|
'font-weight': 400,
|
|
1961
2000
|
fill: muted,
|
|
@@ -1966,7 +2005,7 @@ function renderAttributionMark(model: PositionedRoadmap): string {
|
|
|
1966
2005
|
{
|
|
1967
2006
|
x: ATTRIBUTION_NOW_LOGICAL_X,
|
|
1968
2007
|
y: baselineY,
|
|
1969
|
-
'font-family':
|
|
2008
|
+
'font-family': fonts.sans,
|
|
1970
2009
|
'font-size': ATTRIBUTION_WORDMARK_FONT_SIZE,
|
|
1971
2010
|
'font-weight': 700,
|
|
1972
2011
|
fill: muted,
|
|
@@ -1984,7 +2023,7 @@ function renderAttributionMark(model: PositionedRoadmap): string {
|
|
|
1984
2023
|
{
|
|
1985
2024
|
x: ATTRIBUTION_INE_LOGICAL_X,
|
|
1986
2025
|
y: baselineY,
|
|
1987
|
-
'font-family':
|
|
2026
|
+
'font-family': fonts.sans,
|
|
1988
2027
|
'font-size': ATTRIBUTION_WORDMARK_FONT_SIZE,
|
|
1989
2028
|
'font-weight': 400,
|
|
1990
2029
|
fill: muted,
|
|
@@ -2078,6 +2117,10 @@ export async function renderSvg(
|
|
|
2078
2117
|
const idPrefix = ids.next('root');
|
|
2079
2118
|
|
|
2080
2119
|
const palette = model.palette;
|
|
2120
|
+
// Per-role family strings stamped onto every <text>. Defaults to the
|
|
2121
|
+
// portable FONT_STACK; raster/preview callers pass a pinned bundled
|
|
2122
|
+
// family so the SVG names exactly the font the consumer provides.
|
|
2123
|
+
const fonts: FontFamilies = options.fontFamilies ?? FONT_STACK;
|
|
2081
2124
|
const parts: string[] = [];
|
|
2082
2125
|
|
|
2083
2126
|
// <defs> — shadows + arrowhead markers (palette-driven fills baked in).
|
|
@@ -2110,7 +2153,7 @@ export async function renderSvg(
|
|
|
2110
2153
|
// swimlane background rects emitted later — so the major dotted
|
|
2111
2154
|
// and minor grid lines never actually rendered in the chart body.
|
|
2112
2155
|
// They now ship as their own layer below.
|
|
2113
|
-
parts.push(renderTimeline(model.timeline, palette));
|
|
2156
|
+
parts.push(renderTimeline(model.timeline, palette, fonts));
|
|
2114
2157
|
|
|
2115
2158
|
// Swimlane backgrounds — emitted as their own pass so the grid
|
|
2116
2159
|
// lines can be drawn on top of them, then the swimlane content
|
|
@@ -2136,11 +2179,13 @@ export async function renderSvg(
|
|
|
2136
2179
|
}
|
|
2137
2180
|
|
|
2138
2181
|
// Swimlane content (frame tabs + items) on top of the grid lines.
|
|
2139
|
-
for (const s of model.swimlanes)
|
|
2182
|
+
for (const s of model.swimlanes)
|
|
2183
|
+
parts.push(renderSwimlane(s, options, idPrefix, palette, fonts));
|
|
2140
2184
|
|
|
2141
2185
|
// Include regions (drawn after own swimlanes so the dashed border + tab
|
|
2142
2186
|
// overlay the chart, with their own nested swimlanes inside).
|
|
2143
|
-
for (const r of model.includes)
|
|
2187
|
+
for (const r of model.includes)
|
|
2188
|
+
parts.push(renderIncludeRegion(r, options, idPrefix, palette, fonts));
|
|
2144
2189
|
|
|
2145
2190
|
// Normal / overflow dependency edges on top of items but below
|
|
2146
2191
|
// cut-lines / nowline. Under-bar edges already painted above.
|
|
@@ -2154,16 +2199,16 @@ export async function renderSvg(
|
|
|
2154
2199
|
for (const m of model.milestones) parts.push(renderMilestoneCutLine(m, palette));
|
|
2155
2200
|
|
|
2156
2201
|
// Marker-row diamonds + labels.
|
|
2157
|
-
for (const a of model.anchors) parts.push(renderAnchor(a, palette));
|
|
2158
|
-
for (const m of model.milestones) parts.push(renderMilestone(m, palette));
|
|
2202
|
+
for (const a of model.anchors) parts.push(renderAnchor(a, palette, fonts));
|
|
2203
|
+
for (const m of model.milestones) parts.push(renderMilestone(m, palette, fonts));
|
|
2159
2204
|
|
|
2160
2205
|
// Now-line
|
|
2161
|
-
parts.push(renderNowline(model.nowline, palette));
|
|
2206
|
+
parts.push(renderNowline(model.nowline, palette, fonts));
|
|
2162
2207
|
|
|
2163
2208
|
// Footnotes + header last (always on top)
|
|
2164
|
-
parts.push(renderFootnotes(model.footnotes, idPrefix, palette));
|
|
2165
|
-
parts.push(renderHeader(model.header, idPrefix, palette));
|
|
2166
|
-
parts.push(renderAttributionMark(model));
|
|
2209
|
+
parts.push(renderFootnotes(model.footnotes, idPrefix, palette, fonts));
|
|
2210
|
+
parts.push(renderHeader(model.header, idPrefix, palette, fonts));
|
|
2211
|
+
parts.push(renderAttributionMark(model, fonts));
|
|
2167
2212
|
|
|
2168
2213
|
// Logo (if header carries one)
|
|
2169
2214
|
if (model.header.logo && options.assetResolver) {
|