privateboard 0.1.13 → 0.1.16
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/cli.js +2623 -333
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +6 -6
- package/public/agent-build-bgm.js +292 -0
- package/public/agent-overlay.css +14 -14
- package/public/agent-profile.css +408 -87
- package/public/agent-profile.js +254 -0
- package/public/app.js +2486 -384
- package/public/home.html +26 -26
- package/public/i18n.js +1890 -21
- package/public/icons/logo2.png +0 -0
- package/public/icons/private-board-vi.html +1716 -0
- package/public/index.html +2954 -1018
- package/public/magazine.html +12 -12
- package/public/new-agent.css +29 -29
- package/public/newspaper.html +20 -20
- package/public/onboarding.css +350 -272
- package/public/onboarding.js +614 -323
- package/public/quote-cta.css +4 -4
- package/public/report.html +2008 -1673
- package/public/room-settings.css +192 -24
- package/public/room-settings.js +5 -0
- package/public/share-cover-svg-creator.js +736 -0
- package/public/themes.css +0 -34
- package/public/typing-sfx.js +176 -3
- package/public/user-settings.css +50 -27
- package/public/user-settings.js +43 -14
- package/public/voice-onboarding.css +425 -0
- package/public/voice-onboarding.js +144 -0
- package/public/voice-replay.css +31 -38
- package/public/voice-replay.js +12 -11
package/public/report.html
CHANGED
|
@@ -277,12 +277,6 @@
|
|
|
277
277
|
background: transparent !important;
|
|
278
278
|
border-top-color: #1A1814 !important;
|
|
279
279
|
}
|
|
280
|
-
/* Dark spine's mermaid frame — give it a paper bg in print. */
|
|
281
|
-
body[data-spine="boardroom-dark"] .body pre.mermaid {
|
|
282
|
-
background: #FFFFFF !important;
|
|
283
|
-
border-color: #DDD5C8 !important;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
280
|
/* CJK dispatch handles (cjk-sans / cjk-serif / cjk-mono) are
|
|
287
281
|
declared OUTSIDE this @media block — see the @font-face
|
|
288
282
|
trio at the top of <style>. Per-element font-family rules
|
|
@@ -353,7 +347,7 @@
|
|
|
353
347
|
.body section.section-considerations li.rec-item,
|
|
354
348
|
.body section.section-the-bet > ol > li,
|
|
355
349
|
.body section.section-new-questions li.nq-item,
|
|
356
|
-
.body
|
|
350
|
+
.body figure.kami-chart,
|
|
357
351
|
.body table.md-table,
|
|
358
352
|
/* Tier A additions · the units that were still getting sliced
|
|
359
353
|
between pages despite the legacy ruleset. Each one is a small
|
|
@@ -406,7 +400,7 @@
|
|
|
406
400
|
.body section > h2 + ul,
|
|
407
401
|
.body section > h2 + .pillars-grid,
|
|
408
402
|
.body section > h2 + h3,
|
|
409
|
-
.body section > h2 +
|
|
403
|
+
.body section > h2 + figure.kami-chart,
|
|
410
404
|
.body section > h3 + p,
|
|
411
405
|
.body section > h3 + blockquote,
|
|
412
406
|
.body section > h3 + table.md-table,
|
|
@@ -526,10 +520,10 @@
|
|
|
526
520
|
break-inside: avoid;
|
|
527
521
|
page-break-inside: avoid;
|
|
528
522
|
}
|
|
529
|
-
/*
|
|
530
|
-
is small. */
|
|
531
|
-
.body
|
|
532
|
-
.body
|
|
523
|
+
/* Kami chart figures shouldn't reserve big empty boxes when the
|
|
524
|
+
inline SVG is small. */
|
|
525
|
+
.body figure.kami-chart { padding: 14px !important; }
|
|
526
|
+
.body figure.kami-chart svg.kc-svg { max-width: 100% !important; height: auto !important; }
|
|
533
527
|
}
|
|
534
528
|
|
|
535
529
|
/* ─── Author byline · name list (spine-agnostic) ───────────────
|
|
@@ -598,185 +592,6 @@
|
|
|
598
592
|
border-top: none;
|
|
599
593
|
}
|
|
600
594
|
|
|
601
|
-
/* ─── Mermaid + adjacent table = doubled hairline ───────────────
|
|
602
|
-
Every spine's `pre.mermaid` carries a 1px `border-bottom` (the
|
|
603
|
-
mermaid frame's closing rule). A pipe-table directly underneath
|
|
604
|
-
carries its own 1.5–2px `border-top`. With only a paragraph
|
|
605
|
-
break between, the two parallel rules read as a doubled frame.
|
|
606
|
-
Drop the table's top border in that exact arrangement — the
|
|
607
|
-
mermaid's bottom rule already separates the diagram from the
|
|
608
|
-
table cleanly. Same reasoning works for the reverse order: a
|
|
609
|
-
mermaid right after a table inherits the table's last-row
|
|
610
|
-
border-bottom, so suppress the mermaid's top rule there too. */
|
|
611
|
-
.body pre.mermaid + table.md-table {
|
|
612
|
-
border-top: none;
|
|
613
|
-
}
|
|
614
|
-
.body table.md-table + pre.mermaid {
|
|
615
|
-
border-top-width: 0;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/* ─── Hide mermaid's internal render scratchpad ──────────────
|
|
619
|
-
During `mermaid.render(id, src)`, mermaid 10 briefly appends a
|
|
620
|
-
`<div id="dmermaid-...">` to document.body to measure layout +
|
|
621
|
-
produce the SVG. The div is removed once render() resolves,
|
|
622
|
-
but in the meantime it can flash visibly. Force it offscreen
|
|
623
|
-
so the user never sees mermaid's pre-takeover render at all.
|
|
624
|
-
The element still has natural dimensions for mermaid's layout
|
|
625
|
-
measurement — only its position + visibility is constrained. */
|
|
626
|
-
body > [id^="dmermaid-"] {
|
|
627
|
-
position: absolute !important;
|
|
628
|
-
left: -99999px !important;
|
|
629
|
-
top: 0 !important;
|
|
630
|
-
visibility: hidden !important;
|
|
631
|
-
pointer-events: none !important;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/* ─── Hide mermaid SVG until spine palette is applied ─────────
|
|
635
|
-
Pattern from user reports: "first time wrong, refresh OK" /
|
|
636
|
-
strict alternation (1st wrong, 2nd right, 3rd wrong, 4th
|
|
637
|
-
right). Cause: even with our continuous post-render takeover
|
|
638
|
-
(see report.html post-mermaid.run block), mermaid paints at
|
|
639
|
-
least once with its default palette BEFORE our takeover lands
|
|
640
|
-
— and the user sees that first paint. The takeover then
|
|
641
|
-
corrects it, but the user's already perceived "wrong colors."
|
|
642
|
-
On refresh, cached rendering may finish faster and the first
|
|
643
|
-
paint happens AFTER our takeover, which they perceive as
|
|
644
|
-
correct.
|
|
645
|
-
Defense: hide the SVG inside each pre.mermaid until we add
|
|
646
|
-
the `is-painted` class. The post-render takeover adds the
|
|
647
|
-
class as its last step. Until then, the user sees the frame
|
|
648
|
-
(matching the spine) with no SVG inside — no off-brand
|
|
649
|
-
colors flash at any point. */
|
|
650
|
-
.body pre.mermaid svg {
|
|
651
|
-
visibility: hidden;
|
|
652
|
-
}
|
|
653
|
-
.body pre.mermaid.is-painted svg {
|
|
654
|
-
visibility: visible;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/* ─── CSS-variable mermaid palette · timing-race-immune ──────
|
|
658
|
-
Final defense layer for the recurring "first time wrong,
|
|
659
|
-
refresh OK" bug. JS takeover fights timing with mermaid's
|
|
660
|
-
deferred render work; CSS doesn't have that problem — as
|
|
661
|
-
soon as the SVG is in the DOM, these rules apply. The
|
|
662
|
-
spine-specific colours are injected as CSS variables on
|
|
663
|
-
`:root` from JS in the swapSpine flow (see swapSpine /
|
|
664
|
-
mermaid.initialize block). Variables update when the spine
|
|
665
|
-
changes; CSS rules are static.
|
|
666
|
-
|
|
667
|
-
Specificity: each rule uses a 3-class+ chain plus
|
|
668
|
-
!important, beating mermaid's `.quadrant-fill { fill: ... }`
|
|
669
|
-
and any `fill="..."` presentation attribute. The element
|
|
670
|
-
selectors target every mermaid 10 chart-type's known
|
|
671
|
-
visual elements. */
|
|
672
|
-
|
|
673
|
-
/* Quadrant chart · 4 quadrant rects + data points */
|
|
674
|
-
.body pre.mermaid svg .quadrants .quadrant rect,
|
|
675
|
-
.body pre.mermaid svg g.quadrant > rect,
|
|
676
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] g.quadrant rect {
|
|
677
|
-
fill: var(--mm-q-fill, #1A1A18) !important;
|
|
678
|
-
}
|
|
679
|
-
.body pre.mermaid svg .data-points circle,
|
|
680
|
-
.body pre.mermaid svg circle.data-point,
|
|
681
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .data-points circle {
|
|
682
|
-
fill: var(--mm-q-point, #C9A46B) !important;
|
|
683
|
-
stroke: var(--mm-q-bg, #1A1A18) !important;
|
|
684
|
-
}
|
|
685
|
-
.body pre.mermaid svg .quadrants line,
|
|
686
|
-
.body pre.mermaid svg .axis-line,
|
|
687
|
-
.body pre.mermaid svg .axisl-line,
|
|
688
|
-
.body pre.mermaid svg g.border line,
|
|
689
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] line,
|
|
690
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] g.border line {
|
|
691
|
-
stroke: var(--mm-q-border, #3A3934) !important;
|
|
692
|
-
}
|
|
693
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] text {
|
|
694
|
-
fill: var(--mm-q-text, #C8C5BE) !important;
|
|
695
|
-
}
|
|
696
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .bottom-axis text,
|
|
697
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .left-axis text,
|
|
698
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .top-axis text,
|
|
699
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .right-axis text {
|
|
700
|
-
fill: var(--mm-q-axis-text, #5C5A4D) !important;
|
|
701
|
-
}
|
|
702
|
-
.body pre.mermaid svg [aria-roledescription="quadrantChart"] .quadrant text {
|
|
703
|
-
fill: var(--mm-q-quad-text, #8E8B83) !important;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/* xychart-beta · bars cycle through palette via :nth-child */
|
|
707
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect,
|
|
708
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect {
|
|
709
|
-
fill: var(--mm-pal-0, #C9A46B) !important;
|
|
710
|
-
stroke: none !important;
|
|
711
|
-
}
|
|
712
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(2),
|
|
713
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(2) {
|
|
714
|
-
fill: var(--mm-pal-1, #B6B0A2) !important;
|
|
715
|
-
}
|
|
716
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(3),
|
|
717
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(3) {
|
|
718
|
-
fill: var(--mm-pal-2, #8E8B83) !important;
|
|
719
|
-
}
|
|
720
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(n+4),
|
|
721
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(n+4) {
|
|
722
|
-
fill: var(--mm-pal-3, #5C5A52) !important;
|
|
723
|
-
}
|
|
724
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] text {
|
|
725
|
-
fill: var(--mm-axis-text, #5C5A4D) !important;
|
|
726
|
-
}
|
|
727
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] .background,
|
|
728
|
-
.body pre.mermaid svg [aria-roledescription="xychart"] rect.background {
|
|
729
|
-
fill: transparent !important;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/* Pie · slice palette */
|
|
733
|
-
.body pre.mermaid svg path.pieCircle:nth-of-type(1) { fill: var(--mm-pal-0, #C9A46B) !important; stroke: var(--mm-q-bg, #1A1A18) !important; }
|
|
734
|
-
.body pre.mermaid svg path.pieCircle:nth-of-type(2) { fill: var(--mm-pal-1, #B6B0A2) !important; }
|
|
735
|
-
.body pre.mermaid svg path.pieCircle:nth-of-type(3) { fill: var(--mm-pal-2, #8E8B83) !important; }
|
|
736
|
-
.body pre.mermaid svg path.pieCircle:nth-of-type(n+4) { fill: var(--mm-pal-3, #5C5A52) !important; }
|
|
737
|
-
.body pre.mermaid svg circle.pieOuterCircle {
|
|
738
|
-
stroke: var(--mm-q-border, #3A3934) !important;
|
|
739
|
-
fill: none !important;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/* Universal text safety net inside any mermaid SVG · titleFill
|
|
743
|
-
for anything that escaped the per-type rules above. Lower
|
|
744
|
-
specificity than the type-specific rules so they still win. */
|
|
745
|
-
.body pre.mermaid svg text {
|
|
746
|
-
fill: var(--mm-text, #C8C5BE); /* no !important · type-specific rules above WIN */
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/* ─── Chart display sizes · CSS-enforced ─────────────────────
|
|
750
|
-
Mermaid 10.9.5's chartWidth/chartHeight config keys for
|
|
751
|
-
quadrantChart and xychart are silently ignored by the
|
|
752
|
-
bundled CDN build (verified via DOM dump showing default
|
|
753
|
-
500×500 viewBox even when chartWidth: 460 is configured).
|
|
754
|
-
CSS-enforced sizes via `max-width` on the rendered SVG let
|
|
755
|
-
us get refined-compact dimensions regardless of what
|
|
756
|
-
mermaid's config validator does. The viewBox stays at
|
|
757
|
-
mermaid's internal coordinate space (500×500 etc.) but the
|
|
758
|
-
displayed SVG scales down to our target size. Centered with
|
|
759
|
-
margin: 0 auto so the chart sits flush in the article column
|
|
760
|
-
instead of stretching to full width. */
|
|
761
|
-
.body pre.mermaid svg[aria-roledescription="quadrantChart"] {
|
|
762
|
-
max-width: 460px !important;
|
|
763
|
-
height: auto !important;
|
|
764
|
-
margin: 0 auto !important;
|
|
765
|
-
display: block !important;
|
|
766
|
-
}
|
|
767
|
-
.body pre.mermaid svg[aria-roledescription="xychart"] {
|
|
768
|
-
max-width: 560px !important;
|
|
769
|
-
height: auto !important;
|
|
770
|
-
margin: 0 auto !important;
|
|
771
|
-
display: block !important;
|
|
772
|
-
}
|
|
773
|
-
.body pre.mermaid svg[aria-roledescription="pie"] {
|
|
774
|
-
max-width: 420px !important;
|
|
775
|
-
height: auto !important;
|
|
776
|
-
margin: 0 auto !important;
|
|
777
|
-
display: block !important;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
595
|
/* ─── Metric-strip · spine-agnostic baseline ─────────────────────
|
|
781
596
|
Editorial / Swiss register: oversized thin numerals top-left, mute
|
|
782
597
|
mono label bottom-left, hairline grid (no card backgrounds, no
|
|
@@ -1131,6 +946,487 @@
|
|
|
1131
946
|
color: var(--red, #B5706A);
|
|
1132
947
|
}
|
|
1133
948
|
|
|
949
|
+
/* ─── Kami-chart · spine-agnostic inline-SVG charts ──────────────
|
|
950
|
+
Replaces mermaid as the report's chart-rendering pipeline. Each
|
|
951
|
+
chart is emitted by the writer as a fenced ```kami-chart block
|
|
952
|
+
containing strict JSON; renderKamiChart() in JS turns it into
|
|
953
|
+
inline SVG. Styling is driven entirely by spine CSS variables
|
|
954
|
+
so each spine paints the chart in its own palette — no JS
|
|
955
|
+
palette takeover, no race conditions, no scratchpad div. */
|
|
956
|
+
.body .kami-chart {
|
|
957
|
+
margin: 28px 0 32px;
|
|
958
|
+
padding: 32px 36px 28px;
|
|
959
|
+
background: var(--paper-soft, #ECE6DA);
|
|
960
|
+
border: 1px solid var(--rule, rgba(0,0,0,0.14));
|
|
961
|
+
}
|
|
962
|
+
.body .kami-chart .kc-figheader {
|
|
963
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
964
|
+
padding-bottom: 12px;
|
|
965
|
+
margin-bottom: 18px;
|
|
966
|
+
border-bottom: 1px solid var(--rule, rgba(0,0,0,0.14));
|
|
967
|
+
}
|
|
968
|
+
.body .kami-chart .kc-fignum {
|
|
969
|
+
font-family: var(--mono);
|
|
970
|
+
font-size: 11px;
|
|
971
|
+
text-transform: uppercase;
|
|
972
|
+
letter-spacing: 0.18em;
|
|
973
|
+
color: var(--accent, var(--em, #6B6660));
|
|
974
|
+
font-weight: 600;
|
|
975
|
+
}
|
|
976
|
+
.body .kami-chart .kc-figtitle {
|
|
977
|
+
font-family: var(--mono);
|
|
978
|
+
font-size: 11px;
|
|
979
|
+
text-transform: uppercase;
|
|
980
|
+
letter-spacing: 0.18em;
|
|
981
|
+
color: var(--ink-mid, #6A655D);
|
|
982
|
+
}
|
|
983
|
+
.body .kami-chart svg.kc-svg {
|
|
984
|
+
display: block;
|
|
985
|
+
width: 100%;
|
|
986
|
+
height: auto;
|
|
987
|
+
overflow: visible;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/* Canvas + grid + axis */
|
|
991
|
+
.body .kami-chart .kc-canvas { fill: var(--paper-soft, #ECE6DA); }
|
|
992
|
+
.body .kami-chart .kc-grid { stroke: var(--rule-soft, rgba(0,0,0,0.08)); stroke-width: 0.8; }
|
|
993
|
+
.body .kami-chart .kc-baseline { stroke: var(--ink, #2A2724); stroke-width: 0.8; }
|
|
994
|
+
.body .kami-chart .kc-y-tick {
|
|
995
|
+
fill: var(--ink-soft, #978C7E);
|
|
996
|
+
font: 10px var(--sans);
|
|
997
|
+
}
|
|
998
|
+
.body .kami-chart .kc-y-unit {
|
|
999
|
+
fill: var(--ink-soft, #978C7E);
|
|
1000
|
+
font: 9px var(--mono);
|
|
1001
|
+
letter-spacing: 0.12em;
|
|
1002
|
+
text-transform: uppercase;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/* Bar series · spine accent for primary; neutrals for secondary/tertiary.
|
|
1006
|
+
Single-focal rule from kami's design guide: at most one series carries
|
|
1007
|
+
the accent. Multi-series charts use accent + ink-mid (+ ink-soft). */
|
|
1008
|
+
.body .kami-chart .kc-bar.kc-series-primary { fill: var(--accent, var(--em, #6B6660)); }
|
|
1009
|
+
.body .kami-chart .kc-bar.kc-series-secondary { fill: var(--ink-mid, #6A655D); }
|
|
1010
|
+
.body .kami-chart .kc-bar.kc-series-tertiary { fill: var(--ink-soft, #978C7E); }
|
|
1011
|
+
|
|
1012
|
+
.body .kami-chart .kc-data-label {
|
|
1013
|
+
fill: var(--ink, #2A2724);
|
|
1014
|
+
font: 10px var(--sans);
|
|
1015
|
+
}
|
|
1016
|
+
.body .kami-chart .kc-data-label.kc-primary { font-weight: 600; }
|
|
1017
|
+
.body .kami-chart .kc-category-label {
|
|
1018
|
+
fill: var(--ink, #2A2724);
|
|
1019
|
+
font: 11px var(--sans);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/* Legend */
|
|
1023
|
+
.body .kami-chart .kc-legend-divider { stroke: var(--rule, rgba(0,0,0,0.14)); stroke-width: 0.8; }
|
|
1024
|
+
.body .kami-chart .kc-legend-swatch.kc-series-primary { fill: var(--accent, var(--em, #6B6660)); }
|
|
1025
|
+
.body .kami-chart .kc-legend-swatch.kc-series-secondary { fill: var(--ink-mid, #6A655D); }
|
|
1026
|
+
.body .kami-chart .kc-legend-swatch.kc-series-tertiary { fill: var(--ink-soft, #978C7E); }
|
|
1027
|
+
.body .kami-chart .kc-legend-text {
|
|
1028
|
+
fill: var(--ink-mid, #6A655D);
|
|
1029
|
+
font: 10px var(--sans);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* Caption · editorial gloss below the chart. Serif italic <em> in accent. */
|
|
1033
|
+
.body .kami-chart .kc-caption {
|
|
1034
|
+
font-family: var(--serif);
|
|
1035
|
+
font-size: 14px;
|
|
1036
|
+
color: var(--ink-mid, #6A655D);
|
|
1037
|
+
line-height: 1.7;
|
|
1038
|
+
margin-top: 18px;
|
|
1039
|
+
padding-top: 14px;
|
|
1040
|
+
border-top: 1px solid var(--rule, rgba(0,0,0,0.14));
|
|
1041
|
+
max-width: 60ch;
|
|
1042
|
+
}
|
|
1043
|
+
.body .kami-chart .kc-caption em {
|
|
1044
|
+
color: var(--accent, var(--em, #B65A3A));
|
|
1045
|
+
font-style: italic;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/* Error fallback · mirrors the metric-strip-error register so a
|
|
1049
|
+
malformed chart degrades visibly without blowing up the report. */
|
|
1050
|
+
.body .kami-chart-error {
|
|
1051
|
+
margin: 28px 0;
|
|
1052
|
+
padding: 16px 18px;
|
|
1053
|
+
background: var(--paper-soft, #ECE6DA);
|
|
1054
|
+
border: 1px dashed var(--rule, rgba(0,0,0,0.14));
|
|
1055
|
+
font-family: var(--mono);
|
|
1056
|
+
font-size: 11px;
|
|
1057
|
+
color: var(--ink-soft, #978C7E);
|
|
1058
|
+
text-transform: uppercase;
|
|
1059
|
+
letter-spacing: 0.06em;
|
|
1060
|
+
}
|
|
1061
|
+
.body .kami-chart-error::before {
|
|
1062
|
+
content: "// kami-chart · malformed";
|
|
1063
|
+
display: block;
|
|
1064
|
+
margin-bottom: 6px;
|
|
1065
|
+
color: var(--red, #B5706A);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/* ─── Kami-chart · quadrant ──────────────────────────────────────
|
|
1069
|
+
2-axis plot · plot area framed; preferred (top-right) quadrant
|
|
1070
|
+
gets a subtle accent-tinted fill; crosshair at the midpoint;
|
|
1071
|
+
axis-end arrows; quadrant labels in the four corners. */
|
|
1072
|
+
.body .kami-chart .kc-q-frame { fill: none; stroke: var(--rule, rgba(0,0,0,0.14)); stroke-width: 0.8; }
|
|
1073
|
+
.body .kami-chart .kc-q-axis { stroke: var(--rule-soft, rgba(0,0,0,0.08)); stroke-width: 0.8; }
|
|
1074
|
+
.body .kami-chart .kc-q-tint { fill: var(--paper-deep, rgba(0,0,0,0.03)); }
|
|
1075
|
+
.body .kami-chart .kc-q-arrow { stroke: var(--ink-mid, #6A655D); stroke-width: 1.2; fill: none; }
|
|
1076
|
+
.body .kami-chart .kc-q-axis-label {
|
|
1077
|
+
fill: var(--ink, #2A2724); font: 11px var(--sans);
|
|
1078
|
+
letter-spacing: 0.08em;
|
|
1079
|
+
}
|
|
1080
|
+
.body .kami-chart .kc-q-quad-kicker {
|
|
1081
|
+
fill: var(--ink-soft, #978C7E);
|
|
1082
|
+
font: 9px var(--mono);
|
|
1083
|
+
text-transform: uppercase;
|
|
1084
|
+
letter-spacing: 0.22em;
|
|
1085
|
+
}
|
|
1086
|
+
.body .kami-chart .kc-q-quad-kicker.kc-q-pref { fill: var(--accent, #6B6660); }
|
|
1087
|
+
.body .kami-chart .kc-q-quad-name {
|
|
1088
|
+
fill: var(--ink-mid, #6A655D); font: 10px var(--sans);
|
|
1089
|
+
}
|
|
1090
|
+
.body .kami-chart .kc-q-quad-name.kc-q-pref { fill: var(--accent, #6B6660); font-weight: 500; }
|
|
1091
|
+
.body .kami-chart .kc-point { fill: var(--paper-soft, #ECE6DA); stroke: var(--ink, #2A2724); stroke-width: 1; }
|
|
1092
|
+
.body .kami-chart .kc-point-focal { fill: var(--accent, #6B6660); stroke: var(--accent, #6B6660); stroke-width: 1; }
|
|
1093
|
+
.body .kami-chart .kc-point-tinted { fill: var(--paper-deep, rgba(0,0,0,0.03)); stroke: var(--accent, #6B6660); stroke-width: 1; }
|
|
1094
|
+
.body .kami-chart .kc-point-faint { fill: var(--paper-deep, rgba(0,0,0,0.03)); stroke: var(--ink-muted, #B5AB9B); stroke-width: 1; }
|
|
1095
|
+
.body .kami-chart .kc-point-label {
|
|
1096
|
+
fill: var(--ink, #2A2724); font: 10px var(--sans);
|
|
1097
|
+
}
|
|
1098
|
+
.body .kami-chart .kc-point-label.kc-primary { fill: var(--accent, #6B6660); font-weight: 500; }
|
|
1099
|
+
.body .kami-chart .kc-point-label.kc-faint { fill: var(--ink-mid, #6A655D); }
|
|
1100
|
+
|
|
1101
|
+
/* ─── Kami-chart · timeline ──────────────────────────────────────
|
|
1102
|
+
Horizontal time axis · events alternating above/below · each
|
|
1103
|
+
event = dot on axis + dashed connector + rect card with period
|
|
1104
|
+
(mono) + label (sans). Focal event renders in accent. */
|
|
1105
|
+
.body .kami-chart .kc-t-axis { stroke: var(--ink-muted, #B5AB9B); stroke-width: 1.4; }
|
|
1106
|
+
.body .kami-chart .kc-t-arrow { stroke: var(--ink-muted, #B5AB9B); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1107
|
+
.body .kami-chart .kc-t-dot { fill: var(--ink-mid, #6A655D); stroke: var(--paper-soft, #ECE6DA); stroke-width: 1.5; }
|
|
1108
|
+
.body .kami-chart .kc-t-dot-focal { fill: var(--accent, #6B6660); stroke: var(--paper-soft, #ECE6DA); stroke-width: 1.5; }
|
|
1109
|
+
.body .kami-chart .kc-t-connector {
|
|
1110
|
+
stroke: var(--ink-soft, #978C7E); stroke-width: 1;
|
|
1111
|
+
stroke-dasharray: 3 3;
|
|
1112
|
+
}
|
|
1113
|
+
.body .kami-chart .kc-t-connector-focal {
|
|
1114
|
+
stroke: var(--accent, #6B6660); stroke-width: 1.2;
|
|
1115
|
+
}
|
|
1116
|
+
.body .kami-chart .kc-t-card {
|
|
1117
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1118
|
+
stroke: var(--rule, rgba(0,0,0,0.14));
|
|
1119
|
+
stroke-width: 0.8;
|
|
1120
|
+
}
|
|
1121
|
+
.body .kami-chart .kc-t-card-focal {
|
|
1122
|
+
fill: var(--paper, #F4EFE6);
|
|
1123
|
+
stroke: var(--accent, #6B6660);
|
|
1124
|
+
stroke-width: 1;
|
|
1125
|
+
}
|
|
1126
|
+
.body .kami-chart .kc-t-period {
|
|
1127
|
+
fill: var(--ink-soft, #978C7E);
|
|
1128
|
+
font: 8px var(--mono);
|
|
1129
|
+
letter-spacing: 0.15em;
|
|
1130
|
+
text-transform: uppercase;
|
|
1131
|
+
}
|
|
1132
|
+
.body .kami-chart .kc-t-period-focal {
|
|
1133
|
+
fill: var(--accent, #6B6660); font-weight: 600;
|
|
1134
|
+
}
|
|
1135
|
+
.body .kami-chart .kc-t-label {
|
|
1136
|
+
fill: var(--ink, #2A2724);
|
|
1137
|
+
font: 11px var(--sans); font-weight: 500;
|
|
1138
|
+
}
|
|
1139
|
+
.body .kami-chart .kc-t-label-focal { font-size: 12px; }
|
|
1140
|
+
|
|
1141
|
+
/* ─── Kami-chart · donut ─────────────────────────────────────────
|
|
1142
|
+
6-slice maximum · cx=300 cy=200 R=136 r=76 · clockwise from top.
|
|
1143
|
+
Slices declared in fill order; the largest (slice 1) carries the
|
|
1144
|
+
accent, the rest cascade down the neutral ramp. Hollow centre
|
|
1145
|
+
shows a single value + caption. Legend on the right. */
|
|
1146
|
+
.body .kami-chart .kc-slice-1 { fill: var(--accent, #6B6660); }
|
|
1147
|
+
.body .kami-chart .kc-slice-2 { fill: var(--ink-mid, #6A655D); }
|
|
1148
|
+
.body .kami-chart .kc-slice-3 { fill: var(--ink-soft, #978C7E); }
|
|
1149
|
+
.body .kami-chart .kc-slice-4 { fill: var(--ink-muted, #B5AB9B); }
|
|
1150
|
+
.body .kami-chart .kc-slice-5 { fill: var(--paper-deep, #E2D9C4); }
|
|
1151
|
+
.body .kami-chart .kc-slice-6 {
|
|
1152
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1153
|
+
stroke: var(--rule, rgba(0,0,0,0.14));
|
|
1154
|
+
stroke-width: 0.8;
|
|
1155
|
+
}
|
|
1156
|
+
.body .kami-chart .kc-donut-separator {
|
|
1157
|
+
fill: none;
|
|
1158
|
+
stroke: var(--paper-soft, #ECE6DA);
|
|
1159
|
+
stroke-width: 1.5;
|
|
1160
|
+
}
|
|
1161
|
+
.body .kami-chart .kc-donut-hole {
|
|
1162
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1163
|
+
}
|
|
1164
|
+
.body .kami-chart .kc-donut-center-value {
|
|
1165
|
+
fill: var(--accent, #6B6660);
|
|
1166
|
+
font: 26px var(--serif);
|
|
1167
|
+
font-weight: 500;
|
|
1168
|
+
}
|
|
1169
|
+
.body .kami-chart .kc-donut-center-label {
|
|
1170
|
+
fill: var(--ink-soft, #978C7E);
|
|
1171
|
+
font: 10px var(--sans);
|
|
1172
|
+
letter-spacing: 0.10em;
|
|
1173
|
+
}
|
|
1174
|
+
.body .kami-chart .kc-donut-legend-divider {
|
|
1175
|
+
stroke: var(--rule, rgba(0,0,0,0.14));
|
|
1176
|
+
stroke-width: 0.8;
|
|
1177
|
+
}
|
|
1178
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-1 { fill: var(--accent, #6B6660); }
|
|
1179
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-2 { fill: var(--ink-mid, #6A655D); }
|
|
1180
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-3 { fill: var(--ink-soft, #978C7E); }
|
|
1181
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-4 { fill: var(--ink-muted, #B5AB9B); }
|
|
1182
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-5 { fill: var(--paper-deep, #E2D9C4); }
|
|
1183
|
+
.body .kami-chart .kc-donut-legend-swatch.kc-slice-6 {
|
|
1184
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1185
|
+
stroke: var(--rule, rgba(0,0,0,0.14)); stroke-width: 0.8;
|
|
1186
|
+
}
|
|
1187
|
+
.body .kami-chart .kc-donut-legend-value {
|
|
1188
|
+
fill: var(--ink, #2A2724);
|
|
1189
|
+
font: 9.5px var(--sans);
|
|
1190
|
+
}
|
|
1191
|
+
.body .kami-chart .kc-donut-legend-value.kc-primary { font-weight: 600; }
|
|
1192
|
+
.body .kami-chart .kc-donut-legend-label {
|
|
1193
|
+
fill: var(--ink-mid, #6A655D);
|
|
1194
|
+
font: 9px var(--sans);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ─── Kami-chart · gantt (replaces inline mermaid gantt) ──────────
|
|
1198
|
+
Horizontal time axis with one bar per phase, optionally grouped
|
|
1199
|
+
into named sections. Time scale auto-fits the longest end value.
|
|
1200
|
+
Focal phase renders in accent. */
|
|
1201
|
+
.body .kami-chart .kc-g-section-bg { fill: var(--paper-deep, rgba(0,0,0,0.03)); }
|
|
1202
|
+
.body .kami-chart .kc-g-section-label {
|
|
1203
|
+
fill: var(--ink-soft, #978C7E);
|
|
1204
|
+
font: 9px var(--mono);
|
|
1205
|
+
letter-spacing: 0.16em;
|
|
1206
|
+
text-transform: uppercase;
|
|
1207
|
+
}
|
|
1208
|
+
.body .kami-chart .kc-g-row-rule { stroke: var(--rule-soft, rgba(0,0,0,0.08)); stroke-width: 0.8; }
|
|
1209
|
+
.body .kami-chart .kc-g-task-label {
|
|
1210
|
+
fill: var(--ink, #2A2724);
|
|
1211
|
+
font: 10px var(--sans);
|
|
1212
|
+
}
|
|
1213
|
+
.body .kami-chart .kc-g-task-label.kc-primary { font-weight: 600; }
|
|
1214
|
+
.body .kami-chart .kc-g-bar { fill: var(--ink-mid, #6A655D); }
|
|
1215
|
+
.body .kami-chart .kc-g-bar.kc-primary { fill: var(--accent, #6B6660); }
|
|
1216
|
+
.body .kami-chart .kc-g-bar-label {
|
|
1217
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1218
|
+
font: 9px var(--sans);
|
|
1219
|
+
font-weight: 600;
|
|
1220
|
+
}
|
|
1221
|
+
.body .kami-chart .kc-g-bar-label.kc-outside {
|
|
1222
|
+
fill: var(--ink-mid, #6A655D);
|
|
1223
|
+
}
|
|
1224
|
+
.body .kami-chart .kc-g-time-tick {
|
|
1225
|
+
fill: var(--ink-soft, #978C7E);
|
|
1226
|
+
font: 9px var(--mono);
|
|
1227
|
+
letter-spacing: 0.12em;
|
|
1228
|
+
}
|
|
1229
|
+
.body .kami-chart .kc-g-time-axis { stroke: var(--ink, #2A2724); stroke-width: 0.8; }
|
|
1230
|
+
|
|
1231
|
+
/* ─── Kami-chart · tree (replaces inline mermaid mindmap) ─────────
|
|
1232
|
+
2-deep hierarchy · root + N branches + M leaves per branch.
|
|
1233
|
+
Connectors are right-angle paths with chevron arrows at child tops. */
|
|
1234
|
+
.body .kami-chart .kc-tree-node-bg { fill: var(--paper, #F4EFE6); }
|
|
1235
|
+
.body .kami-chart .kc-tree-root {
|
|
1236
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1237
|
+
stroke: var(--ink, #2A2724);
|
|
1238
|
+
stroke-width: 1;
|
|
1239
|
+
}
|
|
1240
|
+
.body .kami-chart .kc-tree-branch {
|
|
1241
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1242
|
+
stroke: var(--ink-mid, #6A655D);
|
|
1243
|
+
stroke-width: 1;
|
|
1244
|
+
}
|
|
1245
|
+
.body .kami-chart .kc-tree-branch-focal {
|
|
1246
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1247
|
+
stroke: var(--accent, #6B6660);
|
|
1248
|
+
stroke-width: 1;
|
|
1249
|
+
}
|
|
1250
|
+
.body .kami-chart .kc-tree-leaf {
|
|
1251
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1252
|
+
stroke: var(--ink-mid, #6A655D);
|
|
1253
|
+
stroke-width: 1;
|
|
1254
|
+
}
|
|
1255
|
+
.body .kami-chart .kc-tree-leaf-focal {
|
|
1256
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1257
|
+
stroke: var(--accent, #6B6660);
|
|
1258
|
+
stroke-width: 0.8;
|
|
1259
|
+
}
|
|
1260
|
+
.body .kami-chart .kc-tree-kicker {
|
|
1261
|
+
fill: var(--ink-soft, #978C7E);
|
|
1262
|
+
font: 8px var(--mono);
|
|
1263
|
+
letter-spacing: 0.15em;
|
|
1264
|
+
text-transform: uppercase;
|
|
1265
|
+
}
|
|
1266
|
+
.body .kami-chart .kc-tree-kicker-focal { fill: var(--accent, #6B6660); }
|
|
1267
|
+
.body .kami-chart .kc-tree-text {
|
|
1268
|
+
fill: var(--ink, #2A2724);
|
|
1269
|
+
font: 11px var(--sans);
|
|
1270
|
+
font-weight: 500;
|
|
1271
|
+
}
|
|
1272
|
+
.body .kami-chart .kc-tree-connector { stroke: var(--ink-mid, #6A655D); stroke-width: 1.2; fill: none; }
|
|
1273
|
+
.body .kami-chart .kc-tree-connector-focal { stroke: var(--accent, #6B6660); stroke-width: 1.4; fill: none; }
|
|
1274
|
+
.body .kami-chart .kc-tree-chevron { stroke: var(--ink-mid, #6A655D); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1275
|
+
.body .kami-chart .kc-tree-chevron-focal { stroke: var(--accent, #6B6660); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1276
|
+
|
|
1277
|
+
/* ─── Kami-chart · flowchart (replaces inline mermaid flowchart) ──
|
|
1278
|
+
Two layouts:
|
|
1279
|
+
· linear-v · vertical chain of 2–5 nodes
|
|
1280
|
+
· y-decision · root (decision diamond) → 2 branches → optional join
|
|
1281
|
+
Node kinds: start | step | decision | outcome | end. */
|
|
1282
|
+
.body .kami-chart .kc-fl-node {
|
|
1283
|
+
fill: var(--paper, #F4EFE6);
|
|
1284
|
+
stroke: var(--ink, #2A2724);
|
|
1285
|
+
stroke-width: 1;
|
|
1286
|
+
}
|
|
1287
|
+
.body .kami-chart .kc-fl-node-focal {
|
|
1288
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1289
|
+
stroke: var(--accent, #6B6660);
|
|
1290
|
+
stroke-width: 1.2;
|
|
1291
|
+
}
|
|
1292
|
+
.body .kami-chart .kc-fl-node-pill {
|
|
1293
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1294
|
+
stroke: var(--ink-mid, #6A655D);
|
|
1295
|
+
stroke-width: 1;
|
|
1296
|
+
}
|
|
1297
|
+
.body .kami-chart .kc-fl-node-faint {
|
|
1298
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1299
|
+
stroke: var(--ink-muted, #B5AB9B);
|
|
1300
|
+
stroke-width: 1;
|
|
1301
|
+
}
|
|
1302
|
+
.body .kami-chart .kc-fl-kicker {
|
|
1303
|
+
fill: var(--ink-soft, #978C7E);
|
|
1304
|
+
font: 7px var(--mono);
|
|
1305
|
+
letter-spacing: 0.15em;
|
|
1306
|
+
text-transform: uppercase;
|
|
1307
|
+
}
|
|
1308
|
+
.body .kami-chart .kc-fl-kicker-focal { fill: var(--accent, #6B6660); }
|
|
1309
|
+
.body .kami-chart .kc-fl-text {
|
|
1310
|
+
fill: var(--ink, #2A2724);
|
|
1311
|
+
font: 12px var(--sans);
|
|
1312
|
+
font-weight: 500;
|
|
1313
|
+
}
|
|
1314
|
+
.body .kami-chart .kc-fl-text-faint { fill: var(--ink-mid, #6A655D); }
|
|
1315
|
+
.body .kami-chart .kc-fl-arrow { stroke: var(--ink-mid, #6A655D); stroke-width: 1.2; fill: none; }
|
|
1316
|
+
.body .kami-chart .kc-fl-arrow-focal { stroke: var(--accent, #6B6660); stroke-width: 1.4; fill: none; }
|
|
1317
|
+
.body .kami-chart .kc-fl-arrowhead { stroke: var(--ink-mid, #6A655D); stroke-width: 1.4; fill: none; stroke-linecap: round; }
|
|
1318
|
+
.body .kami-chart .kc-fl-arrowhead-focal { stroke: var(--accent, #6B6660); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1319
|
+
.body .kami-chart .kc-fl-edge-label-bg {
|
|
1320
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1321
|
+
}
|
|
1322
|
+
.body .kami-chart .kc-fl-edge-label {
|
|
1323
|
+
fill: var(--ink-mid, #6A655D);
|
|
1324
|
+
font: 8px var(--mono);
|
|
1325
|
+
letter-spacing: 0.14em;
|
|
1326
|
+
text-transform: uppercase;
|
|
1327
|
+
}
|
|
1328
|
+
.body .kami-chart .kc-fl-edge-label.kc-primary {
|
|
1329
|
+
fill: var(--accent, #6B6660);
|
|
1330
|
+
font-weight: 600;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/* ─── Kami-chart · swimlane (replaces sequence + journey) ────────
|
|
1334
|
+
N lanes stacked vertically · steps positioned per lane in left-
|
|
1335
|
+
to-right order · elbow connectors between consecutive steps with
|
|
1336
|
+
chevron arrowheads. Focal lane gets a tinted background; focal
|
|
1337
|
+
step gets accent border. Reuses kc-fl-* classes for nodes. */
|
|
1338
|
+
.body .kami-chart .kc-sw-lane-bg { fill: var(--paper-deep, rgba(0,0,0,0.03)); }
|
|
1339
|
+
.body .kami-chart .kc-sw-lane-bg-alt { fill: var(--paper-soft, #ECE6DA); }
|
|
1340
|
+
.body .kami-chart .kc-sw-lane-bg-focal { fill: var(--paper-deep, rgba(0,0,0,0.05)); }
|
|
1341
|
+
.body .kami-chart .kc-sw-lane-divider { stroke: var(--rule-soft, rgba(0,0,0,0.08)); stroke-width: 0.8; }
|
|
1342
|
+
.body .kami-chart .kc-sw-lane-divider-focal {
|
|
1343
|
+
stroke: var(--accent, #6B6660);
|
|
1344
|
+
stroke-width: 0.8;
|
|
1345
|
+
stroke-dasharray: 4 3;
|
|
1346
|
+
}
|
|
1347
|
+
.body .kami-chart .kc-sw-lane-label {
|
|
1348
|
+
fill: var(--ink-soft, #978C7E);
|
|
1349
|
+
font: 9px var(--mono);
|
|
1350
|
+
letter-spacing: 0.15em;
|
|
1351
|
+
text-transform: uppercase;
|
|
1352
|
+
}
|
|
1353
|
+
.body .kami-chart .kc-sw-lane-label-focal { fill: var(--accent, #6B6660); }
|
|
1354
|
+
.body .kami-chart .kc-sw-label-col {
|
|
1355
|
+
fill: var(--paper, #F4EFE6);
|
|
1356
|
+
stroke: var(--rule, rgba(0,0,0,0.14));
|
|
1357
|
+
stroke-width: 0.8;
|
|
1358
|
+
}
|
|
1359
|
+
.body .kami-chart .kc-sw-arrow { stroke: var(--ink-mid, #6A655D); stroke-width: 1.2; fill: none; }
|
|
1360
|
+
.body .kami-chart .kc-sw-arrow-focal { stroke: var(--accent, #6B6660); stroke-width: 1.4; fill: none; }
|
|
1361
|
+
.body .kami-chart .kc-sw-arrowhead { stroke: var(--ink-mid, #6A655D); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1362
|
+
.body .kami-chart .kc-sw-arrowhead-focal { stroke: var(--accent, #6B6660); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1363
|
+
.body .kami-chart .kc-sw-score-dot {
|
|
1364
|
+
fill: var(--accent, #6B6660);
|
|
1365
|
+
}
|
|
1366
|
+
.body .kami-chart .kc-sw-score-dot-faint {
|
|
1367
|
+
fill: var(--ink-muted, #B5AB9B);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/* ─── Kami-chart · state-machine ──────────────────────────────────
|
|
1371
|
+
Linear arrangement of state nodes · forward arrows (with optional
|
|
1372
|
+
event labels) · optional back-transitions drawn as dashed arcs
|
|
1373
|
+
above. Start dot + end double-circle markers are optional. */
|
|
1374
|
+
.body .kami-chart .kc-sm-state {
|
|
1375
|
+
fill: var(--paper, #F4EFE6);
|
|
1376
|
+
stroke: var(--ink-mid, #6A655D);
|
|
1377
|
+
stroke-width: 1;
|
|
1378
|
+
}
|
|
1379
|
+
.body .kami-chart .kc-sm-state-focal {
|
|
1380
|
+
fill: var(--paper-deep, rgba(0,0,0,0.03));
|
|
1381
|
+
stroke: var(--accent, #6B6660);
|
|
1382
|
+
stroke-width: 1.2;
|
|
1383
|
+
}
|
|
1384
|
+
.body .kami-chart .kc-sm-state-terminal {
|
|
1385
|
+
fill: var(--paper-soft, #ECE6DA);
|
|
1386
|
+
stroke: var(--ink, #2A2724);
|
|
1387
|
+
stroke-width: 1;
|
|
1388
|
+
}
|
|
1389
|
+
.body .kami-chart .kc-sm-kicker {
|
|
1390
|
+
fill: var(--ink-soft, #978C7E);
|
|
1391
|
+
font: 7px var(--mono);
|
|
1392
|
+
letter-spacing: 0.15em;
|
|
1393
|
+
text-transform: uppercase;
|
|
1394
|
+
}
|
|
1395
|
+
.body .kami-chart .kc-sm-kicker-focal { fill: var(--accent, #6B6660); }
|
|
1396
|
+
.body .kami-chart .kc-sm-state-label {
|
|
1397
|
+
fill: var(--ink, #2A2724);
|
|
1398
|
+
font: 12px var(--sans);
|
|
1399
|
+
font-weight: 500;
|
|
1400
|
+
}
|
|
1401
|
+
.body .kami-chart .kc-sm-state-hint {
|
|
1402
|
+
fill: var(--ink-mid, #6A655D);
|
|
1403
|
+
font: 9px var(--mono);
|
|
1404
|
+
}
|
|
1405
|
+
.body .kami-chart .kc-sm-forward { stroke: var(--ink-mid, #6A655D); stroke-width: 1.2; fill: none; }
|
|
1406
|
+
.body .kami-chart .kc-sm-forward-focal { stroke: var(--accent, #6B6660); stroke-width: 1.4; fill: none; }
|
|
1407
|
+
.body .kami-chart .kc-sm-back {
|
|
1408
|
+
stroke: var(--ink-mid, #6A655D);
|
|
1409
|
+
stroke-width: 1.2;
|
|
1410
|
+
fill: none;
|
|
1411
|
+
stroke-dasharray: 4 3;
|
|
1412
|
+
}
|
|
1413
|
+
.body .kami-chart .kc-sm-arrowhead { stroke: var(--ink-mid, #6A655D); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1414
|
+
.body .kami-chart .kc-sm-arrowhead-focal { stroke: var(--accent, #6B6660); stroke-width: 1.5; fill: none; stroke-linecap: round; }
|
|
1415
|
+
.body .kami-chart .kc-sm-transition-label-bg { fill: var(--paper-soft, #ECE6DA); }
|
|
1416
|
+
.body .kami-chart .kc-sm-transition-label {
|
|
1417
|
+
fill: var(--ink-mid, #6A655D);
|
|
1418
|
+
font: 8px var(--mono);
|
|
1419
|
+
letter-spacing: 0.10em;
|
|
1420
|
+
text-transform: uppercase;
|
|
1421
|
+
}
|
|
1422
|
+
.body .kami-chart .kc-sm-transition-label.kc-primary {
|
|
1423
|
+
fill: var(--accent, #6B6660);
|
|
1424
|
+
font-weight: 600;
|
|
1425
|
+
}
|
|
1426
|
+
.body .kami-chart .kc-sm-marker-start { fill: var(--ink, #2A2724); }
|
|
1427
|
+
.body .kami-chart .kc-sm-marker-end-outer { fill: none; stroke: var(--ink, #2A2724); stroke-width: 1.4; }
|
|
1428
|
+
.body .kami-chart .kc-sm-marker-end-inner { fill: var(--ink, #2A2724); }
|
|
1429
|
+
|
|
1134
1430
|
/* ─── Views-compared · spine-agnostic baseline ──────────────────
|
|
1135
1431
|
The room's social map · always rendered when ≥ 2 directors.
|
|
1136
1432
|
Layout: intro → 2-column compare grid (alignment | divergence)
|
|
@@ -1768,39 +2064,105 @@
|
|
|
1768
2064
|
`body[data-spine="…"]` for higher specificity. */
|
|
1769
2065
|
|
|
1770
2066
|
:root {
|
|
1771
|
-
/* Type scale · 1 source of truth across all spines
|
|
1772
|
-
All sizes in px; line-heights unitless; tracking in em.
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
--rep-
|
|
1777
|
-
--rep-
|
|
1778
|
-
--rep-
|
|
1779
|
-
--rep-
|
|
1780
|
-
--rep-
|
|
1781
|
-
--rep-
|
|
1782
|
-
--rep-
|
|
1783
|
-
|
|
1784
|
-
--rep-
|
|
1785
|
-
--rep-
|
|
1786
|
-
--rep-
|
|
1787
|
-
--rep-
|
|
1788
|
-
|
|
1789
|
-
--rep-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
--rep-
|
|
1796
|
-
--rep-
|
|
1797
|
-
--rep-
|
|
1798
|
-
--rep-
|
|
2067
|
+
/* ─── Type scale · 1 source of truth across all spines ─────────
|
|
2068
|
+
All sizes in px; line-heights unitless; tracking in em.
|
|
2069
|
+
Adapted from kami's pt-based ladder (px ≈ pt × 1.33). Spines
|
|
2070
|
+
MAY scale globally (e.g. dense vs lush) but the relative
|
|
2071
|
+
proportions and the line-height/tracking pairings stay locked. */
|
|
2072
|
+
--rep-display: 44px; /* cover title · pt-equiv 33 */
|
|
2073
|
+
--rep-h1: 30px; /* chapter / part divider · pt-equiv 22 */
|
|
2074
|
+
--rep-h2: 24px; /* section headings · pt-equiv 18 */
|
|
2075
|
+
--rep-h3: 18px; /* sub-section + recommendation action · pt-equiv 13 */
|
|
2076
|
+
--rep-h4: 15px; /* small headings · pt-equiv 11 */
|
|
2077
|
+
--rep-lede: 17px; /* intro paragraphs (first ¶ after H1/H2) · pt-equiv 13 */
|
|
2078
|
+
--rep-body: 16px; /* reading prose · pt-equiv 12 */
|
|
2079
|
+
--rep-body-dense: 14px; /* compact body (sidebars, dense lists) · pt-equiv 10.5 */
|
|
2080
|
+
--rep-rationale: 16px; /* secondary prose · matches body */
|
|
2081
|
+
--rep-meta: 13px; /* meta strips, metadata rows · pt-equiv 10 */
|
|
2082
|
+
--rep-pullquote: 19px; /* italic pull-quotes inside cards · pt-equiv 14 */
|
|
2083
|
+
--rep-caption: 11px; /* footer captions · pt-equiv 8 */
|
|
2084
|
+
--rep-label: 10px; /* mono uppercase labels (kickers) · pt-equiv 7.5 */
|
|
2085
|
+
--rep-tiny: 9px; /* footer meta / colophon / minor numerals · pt-equiv 7 */
|
|
2086
|
+
|
|
2087
|
+
/* ─── Line-height · 5 tiers, indexed by content type ───────────
|
|
2088
|
+
Kami's invariant: print is tighter than web body. ≥ 1.6 floats;
|
|
2089
|
+
≤ 1.10 collides. Body prose 1.55-1.70; dense rhythm 1.42-1.45;
|
|
2090
|
+
headlines 1.10-1.30. */
|
|
2091
|
+
--rep-leading-display: 1.18; /* display, H1, H2 */
|
|
2092
|
+
--rep-leading-heading: 1.40; /* H3, H4, lede */
|
|
2093
|
+
--rep-leading-body: 1.70; /* reading prose (loose, English-leaning) */
|
|
2094
|
+
--rep-leading-tight: 1.55; /* secondary prose, captions */
|
|
2095
|
+
--rep-leading-dense: 1.42; /* compact lists, sidebars · kami's "dense body" tier */
|
|
2096
|
+
|
|
2097
|
+
/* ─── Letter-spacing (tracking) · per-role, with CJK comp ──────
|
|
2098
|
+
Latin body sits at 0. CJK body (especially TsangerJinKai02 /
|
|
2099
|
+
PingFang) wants 0.03-0.06em to open up density. Display CJK
|
|
2100
|
+
takes a smaller bump (the glyphs are already large). Mono
|
|
2101
|
+
labels get 0.18-0.20em uppercase tracking. */
|
|
2102
|
+
--rep-tracking-display: -0.012em;
|
|
2103
|
+
--rep-tracking-body: -0.005em;
|
|
2104
|
+
--rep-tracking-cjk-body: 0.04em; /* applied via body.is-cjk */
|
|
2105
|
+
--rep-tracking-cjk-display: 0.02em; /* applied via body.is-cjk to display */
|
|
2106
|
+
--rep-tracking-mono: 0.04em;
|
|
2107
|
+
--rep-tracking-label: 0.20em; /* uppercase kickers / overlines */
|
|
2108
|
+
|
|
2109
|
+
/* ─── Spacing scale · 7-tier, base 4px ─────────────────────────
|
|
2110
|
+
Mirrors kami's xs/sm/md/lg/xl/2xl/3xl. Semantic aliases
|
|
2111
|
+
(--rep-section-gap etc.) below resolve to specific tiers — use
|
|
2112
|
+
the aliases in component CSS, never the bare tier number, so
|
|
2113
|
+
the rhythm stays semantic not arithmetic. */
|
|
2114
|
+
--rep-space-xs: 4px; /* inline adjacent · row internal padding */
|
|
2115
|
+
--rep-space-sm: 8px; /* tag padding · dense layout · row gap */
|
|
2116
|
+
--rep-space-md: 12px; /* component interior · paragraph gap */
|
|
2117
|
+
--rep-space-lg: 20px; /* between components · card padding */
|
|
2118
|
+
--rep-space-xl: 32px; /* section-title margins · between rich items */
|
|
2119
|
+
--rep-space-2xl: 56px; /* between major sections (H2 zones) */
|
|
2120
|
+
--rep-space-3xl: 96px; /* between chapters in long-doc / part covers */
|
|
2121
|
+
|
|
2122
|
+
/* Semantic aliases · use these in component CSS so the rhythm
|
|
2123
|
+
stays intention-named. Add new aliases when a new semantic
|
|
2124
|
+
use emerges; never invent new numeric values. */
|
|
2125
|
+
--rep-section-gap: var(--rep-space-2xl); /* before each H2 */
|
|
2126
|
+
--rep-item-gap: var(--rep-space-xl); /* between rich list items */
|
|
2127
|
+
--rep-para-gap: var(--rep-space-md); /* between paragraphs · 12px */
|
|
2128
|
+
--rep-row-gap: var(--rep-space-sm); /* tight rows inside a card */
|
|
2129
|
+
|
|
2130
|
+
/* ─── Radius scale · capped at 10px ────────────────────────────
|
|
2131
|
+
Per project rule (CLAUDE.md memory · feedback_no_emoji_in_designs
|
|
2132
|
+
adjacency): max 10px. Beyond, surfaces start to feel like App
|
|
2133
|
+
Store chrome. Reserve 16px exclusively for hero / cover cards. */
|
|
2134
|
+
--rep-radius-tight: 2px; /* swatches, mini-tags, inline pills */
|
|
2135
|
+
--rep-radius-card: 4px; /* tag pads, code blocks */
|
|
2136
|
+
--rep-radius-container: 6px; /* card edges, callout aside */
|
|
2137
|
+
--rep-radius-feature: 10px; /* feature cards, lockup tiles */
|
|
2138
|
+
--rep-radius-hero: 16px; /* RESERVED · cover hero only */
|
|
2139
|
+
|
|
2140
|
+
/* ─── Depth · three methods, no hard drop shadows ──────────────
|
|
2141
|
+
Kami: ring shadow / whisper shadow / light-dark surface
|
|
2142
|
+
alternation. Never `0 6px 12px rgba(0,0,0,0.4)` style drops —
|
|
2143
|
+
reads as 2010s material-design and clashes with the editorial
|
|
2144
|
+
register. */
|
|
2145
|
+
--rep-shadow-ring: 0 0 0 1px var(--rule, rgba(0,0,0,0.14));
|
|
2146
|
+
--rep-shadow-whisper: 0 4px 24px rgba(0,0,0,0.05);
|
|
1799
2147
|
|
|
1800
2148
|
/* Reading column */
|
|
1801
2149
|
--rep-prose-width: 64ch;
|
|
1802
2150
|
}
|
|
1803
2151
|
|
|
2152
|
+
/* ─── CJK body letter-spacing application ──────────────────────
|
|
2153
|
+
The CJK tracking tokens above are inert until we apply them.
|
|
2154
|
+
`body.is-cjk` is set by report.js when the brief's locale is
|
|
2155
|
+
zh/ja, so non-CJK reports remain untouched. */
|
|
2156
|
+
body.is-cjk .body p,
|
|
2157
|
+
body.is-cjk .body li,
|
|
2158
|
+
body.is-cjk .body blockquote,
|
|
2159
|
+
body.is-cjk .body td,
|
|
2160
|
+
body.is-cjk .body th { letter-spacing: var(--rep-tracking-cjk-body); }
|
|
2161
|
+
body.is-cjk .body h1,
|
|
2162
|
+
body.is-cjk .body h2,
|
|
2163
|
+
body.is-cjk .body h3,
|
|
2164
|
+
body.is-cjk .body .cover-title { letter-spacing: var(--rep-tracking-cjk-display); }
|
|
2165
|
+
|
|
1804
2166
|
/* ── Section break + heading rhythm · cross-spine ────────────────
|
|
1805
2167
|
Each numbered section is preceded by `<div class="chapter-num">
|
|
1806
2168
|
Section NN</div>` (injected client-side). Together they form the
|
|
@@ -1994,7 +2356,7 @@
|
|
|
1994
2356
|
}
|
|
1995
2357
|
.rec-num {
|
|
1996
2358
|
font-family: var(--mono);
|
|
1997
|
-
font-size:
|
|
2359
|
+
font-size: 10px;
|
|
1998
2360
|
font-weight: 400;
|
|
1999
2361
|
color: var(--ink-faint, var(--text-faint));
|
|
2000
2362
|
letter-spacing: 0.2em;
|
|
@@ -3020,73 +3382,6 @@
|
|
|
3020
3382
|
.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
3021
3383
|
}
|
|
3022
3384
|
|
|
3023
|
-
/** Mermaid 10.9.5 quadrantChart lexer is strict: anything beyond ASCII
|
|
3024
|
-
* alphanumerics + spaces breaks UNQUOTED axis/quadrant/item labels
|
|
3025
|
-
* (CJK, parens, `+`, etc. all fail with "Unrecognized text"). The
|
|
3026
|
-
* fix is to ALWAYS wrap those labels in double quotes; titles are
|
|
3027
|
-
* exempt. Mirrors src/utils/mermaid-sanitize.ts — keep in sync. */
|
|
3028
|
-
function sanitizeMermaid(src) {
|
|
3029
|
-
if (!src) return src;
|
|
3030
|
-
if (!/^\s*quadrantChart\b/i.test(src)) return src;
|
|
3031
|
-
|
|
3032
|
-
const clamp01 = (n) => {
|
|
3033
|
-
if (!Number.isFinite(n)) return 0.5;
|
|
3034
|
-
return Math.max(0.02, Math.min(0.98, n));
|
|
3035
|
-
};
|
|
3036
|
-
const cleanLabel = (s) => s
|
|
3037
|
-
.replace(/(/g, "(")
|
|
3038
|
-
.replace(/)/g, ")")
|
|
3039
|
-
.replace(/,/g, " ")
|
|
3040
|
-
.replace(/:/g, " ")
|
|
3041
|
-
.replace(/、/g, " ")
|
|
3042
|
-
.replace(/。/g, " ")
|
|
3043
|
-
.replace(/;/g, " ")
|
|
3044
|
-
.replace(/["'`\[\]:]+/g, " ")
|
|
3045
|
-
.replace(/\s+/g, " ")
|
|
3046
|
-
.trim();
|
|
3047
|
-
|
|
3048
|
-
return src.split("\n").map((line) => {
|
|
3049
|
-
const indentMatch = /^(\s*)/.exec(line);
|
|
3050
|
-
const indent = indentMatch ? indentMatch[1] || " " : " ";
|
|
3051
|
-
const t = line.trim();
|
|
3052
|
-
if (!t) return line;
|
|
3053
|
-
|
|
3054
|
-
// Title · cleaned but unquoted.
|
|
3055
|
-
const titleM = /^title\s+(.+)$/i.exec(t);
|
|
3056
|
-
if (titleM) return `${indent}title ${cleanLabel(titleM[1])}`;
|
|
3057
|
-
|
|
3058
|
-
// Axis lines · always quoted both-ends.
|
|
3059
|
-
const ax = /^(x-axis|y-axis)\s+(.+)$/i.exec(t);
|
|
3060
|
-
if (ax) {
|
|
3061
|
-
const which = ax[1].toLowerCase();
|
|
3062
|
-
const rest = ax[2].trim();
|
|
3063
|
-
if (rest.includes("-->")) {
|
|
3064
|
-
const parts = rest.split("-->").map((s) => cleanLabel(s));
|
|
3065
|
-
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
3066
|
-
return `${indent}${which} "${parts[0]}" --> "${parts[1]}"`;
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3069
|
-
const cleaned = cleanLabel(rest);
|
|
3070
|
-
return `${indent}${which} "Low ${cleaned}" --> "High ${cleaned}"`;
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
// Quadrant labels · always quoted.
|
|
3074
|
-
const qM = /^(quadrant-[1-4])\s+(.+)$/i.exec(t);
|
|
3075
|
-
if (qM) return `${indent}${qM[1]} "${cleanLabel(qM[2])}"`;
|
|
3076
|
-
|
|
3077
|
-
// Item lines · always quoted, coords clamped.
|
|
3078
|
-
const item = /^"?([^"\[\]]+?)"?\s*:\s*\[\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\]\s*$/.exec(t);
|
|
3079
|
-
if (item) {
|
|
3080
|
-
const label = cleanLabel(item[1]);
|
|
3081
|
-
const x = clamp01(parseFloat(item[2]));
|
|
3082
|
-
const y = clamp01(parseFloat(item[3]));
|
|
3083
|
-
return `${indent}"${label}": [${x.toFixed(2)}, ${y.toFixed(2)}]`;
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
return line;
|
|
3087
|
-
}).join("\n");
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
3385
|
/** Parse a fenced ```metric-strip block body (strict JSON) and emit
|
|
3091
3386
|
* the dashboard card grid. Mirrors how ```mermaid is handled — the
|
|
3092
3387
|
* block is pulled out at the placeholder layer before block parsing,
|
|
@@ -3225,6 +3520,1415 @@
|
|
|
3225
3520
|
`</div>`;
|
|
3226
3521
|
}
|
|
3227
3522
|
|
|
3523
|
+
/** Parse a fenced ```kami-chart block (strict JSON) and emit inline
|
|
3524
|
+
* SVG. Replaces the mermaid pipeline for typed visuals. Dispatch by
|
|
3525
|
+
* `type` field; each generator owns its own SVG coordinate math and
|
|
3526
|
+
* uses spine CSS variables for colour so per-spine palettes apply
|
|
3527
|
+
* automatically. Bad JSON / unknown type degrades to a quiet error
|
|
3528
|
+
* block · never throws. */
|
|
3529
|
+
function renderKamiChart(body) {
|
|
3530
|
+
let parsed;
|
|
3531
|
+
try { parsed = JSON.parse(body); } catch (e) {
|
|
3532
|
+
return `<div class="kami-chart-error" data-err="parse">${escape(body)}</div>`;
|
|
3533
|
+
}
|
|
3534
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3535
|
+
return `<div class="kami-chart-error" data-err="shape"></div>`;
|
|
3536
|
+
}
|
|
3537
|
+
const type = typeof parsed.type === "string" ? parsed.type.trim() : "";
|
|
3538
|
+
switch (type) {
|
|
3539
|
+
case "bar":
|
|
3540
|
+
case "bar-chart":
|
|
3541
|
+
return renderKamiBar(parsed);
|
|
3542
|
+
case "quadrant":
|
|
3543
|
+
case "quadrant-chart":
|
|
3544
|
+
return renderKamiQuadrant(parsed);
|
|
3545
|
+
case "timeline":
|
|
3546
|
+
return renderKamiTimeline(parsed);
|
|
3547
|
+
case "donut":
|
|
3548
|
+
case "donut-chart":
|
|
3549
|
+
case "pie":
|
|
3550
|
+
case "pie-chart":
|
|
3551
|
+
return renderKamiDonut(parsed);
|
|
3552
|
+
case "gantt":
|
|
3553
|
+
return renderKamiGantt(parsed);
|
|
3554
|
+
case "tree":
|
|
3555
|
+
case "mindmap":
|
|
3556
|
+
return renderKamiTree(parsed);
|
|
3557
|
+
case "flowchart":
|
|
3558
|
+
case "flow":
|
|
3559
|
+
return renderKamiFlowchart(parsed);
|
|
3560
|
+
case "swimlane":
|
|
3561
|
+
case "sequence":
|
|
3562
|
+
case "journey":
|
|
3563
|
+
return renderKamiSwimlane(parsed);
|
|
3564
|
+
case "state":
|
|
3565
|
+
case "state-machine":
|
|
3566
|
+
case "statediagram":
|
|
3567
|
+
case "statediagram-v2":
|
|
3568
|
+
return renderKamiStateMachine(parsed);
|
|
3569
|
+
default:
|
|
3570
|
+
return `<div class="kami-chart-error" data-err="type">unsupported chart type: ${escape(type || "<empty>")}</div>`;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
/** Pick the next "nice" round number ≥ v · 1, 1.5, 2, 2.5, 3, 4, 5,
|
|
3575
|
+
* 7.5, 10 × 10^N. Used to round the bar-chart Y-axis maximum so
|
|
3576
|
+
* gridlines hit even values regardless of the underlying data. */
|
|
3577
|
+
function kcNiceMax(v) {
|
|
3578
|
+
if (!isFinite(v) || v <= 0) return 10;
|
|
3579
|
+
const exp = Math.floor(Math.log10(v));
|
|
3580
|
+
const pow = Math.pow(10, exp);
|
|
3581
|
+
const norm = v / pow;
|
|
3582
|
+
let nice;
|
|
3583
|
+
if (norm <= 1) nice = 1;
|
|
3584
|
+
else if (norm <= 1.5) nice = 1.5;
|
|
3585
|
+
else if (norm <= 2) nice = 2;
|
|
3586
|
+
else if (norm <= 2.5) nice = 2.5;
|
|
3587
|
+
else if (norm <= 3) nice = 3;
|
|
3588
|
+
else if (norm <= 4) nice = 4;
|
|
3589
|
+
else if (norm <= 5) nice = 5;
|
|
3590
|
+
else if (norm <= 7.5) nice = 7.5;
|
|
3591
|
+
else nice = 10;
|
|
3592
|
+
return nice * pow;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
/** Format a Y-axis tick value · drop trailing zeros, keep ≤ 2
|
|
3596
|
+
* decimals so labels read cleanly at 10px. */
|
|
3597
|
+
function kcFmt(v) {
|
|
3598
|
+
if (!isFinite(v)) return "";
|
|
3599
|
+
if (Number.isInteger(v)) return String(v);
|
|
3600
|
+
return Number(v.toFixed(2)).toString();
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
/** Bar chart · 2–8 categories, single series. Optional `focal` label
|
|
3604
|
+
* picks one bar to render with the spine accent (others go neutral).
|
|
3605
|
+
* Without `focal`, every bar is primary. */
|
|
3606
|
+
function renderKamiBar(data) {
|
|
3607
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
3608
|
+
const yLabel = typeof data.yLabel === "string" ? data.yLabel.trim() : "";
|
|
3609
|
+
const unit = typeof data.unit === "string" ? data.unit.trim() : "";
|
|
3610
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
3611
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
3612
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
3613
|
+
? String(data.figNum) : "";
|
|
3614
|
+
const barsRaw = Array.isArray(data.bars) ? data.bars : [];
|
|
3615
|
+
const bars = [];
|
|
3616
|
+
for (const b of barsRaw) {
|
|
3617
|
+
if (!b || typeof b !== "object") continue;
|
|
3618
|
+
const label = typeof b.label === "string" ? b.label.trim() : "";
|
|
3619
|
+
const value = Number(b.value);
|
|
3620
|
+
if (!label || !isFinite(value)) continue;
|
|
3621
|
+
bars.push({ label, value });
|
|
3622
|
+
if (bars.length >= 8) break;
|
|
3623
|
+
}
|
|
3624
|
+
if (bars.length < 2) {
|
|
3625
|
+
return `<div class="kami-chart-error" data-err="too-few-bars"></div>`;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// Geometry · viewBox 680 × 420, chart area x∈[60,620], y∈[40,320].
|
|
3629
|
+
const X0 = 60, X1 = 620, Y0 = 40, Y1 = 320;
|
|
3630
|
+
const W = X1 - X0; // 560
|
|
3631
|
+
const H = Y1 - Y0; // 280
|
|
3632
|
+
const N = bars.length;
|
|
3633
|
+
const groupW = W / N;
|
|
3634
|
+
const barW = Math.min(48, Math.round(groupW * 0.55));
|
|
3635
|
+
const maxV = Math.max(...bars.map((b) => b.value), 0);
|
|
3636
|
+
const yMax = kcNiceMax(maxV);
|
|
3637
|
+
|
|
3638
|
+
// Five gridlines at 0 / 25% / 50% / 75% / 100% of yMax.
|
|
3639
|
+
const gridlines = [0, 0.25, 0.5, 0.75, 1].map((p) => ({
|
|
3640
|
+
v: yMax * p,
|
|
3641
|
+
y: Y1 - (Y1 - Y0) * p,
|
|
3642
|
+
}));
|
|
3643
|
+
|
|
3644
|
+
// Per-bar layout
|
|
3645
|
+
const barEls = [];
|
|
3646
|
+
const labelEls = [];
|
|
3647
|
+
const dataLabelEls = [];
|
|
3648
|
+
bars.forEach((b, i) => {
|
|
3649
|
+
const cx = X0 + groupW * i + groupW / 2;
|
|
3650
|
+
const bx = Math.round(cx - barW / 2);
|
|
3651
|
+
const bh = Math.round((b.value / yMax) * H);
|
|
3652
|
+
const by = Y1 - bh;
|
|
3653
|
+
const isFocal = focal ? (b.label === focal) : true;
|
|
3654
|
+
const seriesCls = isFocal ? "kc-series-primary" : "kc-series-secondary";
|
|
3655
|
+
barEls.push(
|
|
3656
|
+
`<rect class="kc-bar ${seriesCls}" x="${bx}" y="${by}" width="${barW}" height="${bh}" rx="2"/>`,
|
|
3657
|
+
);
|
|
3658
|
+
// Number above bar
|
|
3659
|
+
const labelTxt = unit
|
|
3660
|
+
? `${kcFmt(b.value)}${unit}`
|
|
3661
|
+
: kcFmt(b.value);
|
|
3662
|
+
dataLabelEls.push(
|
|
3663
|
+
`<text class="kc-data-label${isFocal ? " kc-primary" : ""}" x="${Math.round(cx)}" y="${by - 4}" text-anchor="middle">${escape(labelTxt)}</text>`,
|
|
3664
|
+
);
|
|
3665
|
+
// Category label · truncate to 18 chars to keep packing safe
|
|
3666
|
+
const catLabel = b.label.length > 18 ? b.label.slice(0, 17) + "…" : b.label;
|
|
3667
|
+
labelEls.push(
|
|
3668
|
+
`<text class="kc-category-label" x="${Math.round(cx)}" y="${Y1 + 20}" text-anchor="middle">${escape(catLabel)}</text>`,
|
|
3669
|
+
);
|
|
3670
|
+
});
|
|
3671
|
+
|
|
3672
|
+
// Gridlines + Y-axis ticks
|
|
3673
|
+
const gridEls = gridlines.map((g) =>
|
|
3674
|
+
`<line class="kc-grid" x1="${X0}" y1="${g.y}" x2="${X1}" y2="${g.y}"/>`,
|
|
3675
|
+
).join("");
|
|
3676
|
+
const tickEls = gridlines.map((g) =>
|
|
3677
|
+
`<text class="kc-y-tick" x="${X0 - 8}" y="${g.y + 4}" text-anchor="end">${escape(kcFmt(g.v))}</text>`,
|
|
3678
|
+
).join("");
|
|
3679
|
+
|
|
3680
|
+
// Y-axis unit (rotated)
|
|
3681
|
+
const yUnitTxt = yLabel || unit;
|
|
3682
|
+
const yUnitEl = yUnitTxt
|
|
3683
|
+
? `<text class="kc-y-unit" x="16" y="${(Y0 + Y1) / 2}" transform="rotate(-90 16 ${(Y0 + Y1) / 2})" text-anchor="middle">${escape(yUnitTxt.toUpperCase())}</text>`
|
|
3684
|
+
: "";
|
|
3685
|
+
|
|
3686
|
+
// Figure header
|
|
3687
|
+
const figHeader =
|
|
3688
|
+
`<div class="kc-figheader">` +
|
|
3689
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
3690
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
3691
|
+
`</div>`;
|
|
3692
|
+
|
|
3693
|
+
// Caption
|
|
3694
|
+
const captionEl = caption
|
|
3695
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
3696
|
+
: "";
|
|
3697
|
+
|
|
3698
|
+
return (
|
|
3699
|
+
`<figure class="kami-chart">` +
|
|
3700
|
+
figHeader +
|
|
3701
|
+
`<svg class="kc-svg" viewBox="0 0 680 380" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Bar chart")}">` +
|
|
3702
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
3703
|
+
gridEls +
|
|
3704
|
+
tickEls +
|
|
3705
|
+
yUnitEl +
|
|
3706
|
+
`<line class="kc-baseline" x1="${X0}" y1="${Y1}" x2="${X1}" y2="${Y1}"/>` +
|
|
3707
|
+
barEls.join("") +
|
|
3708
|
+
dataLabelEls.join("") +
|
|
3709
|
+
labelEls.join("") +
|
|
3710
|
+
`</svg>` +
|
|
3711
|
+
captionEl +
|
|
3712
|
+
`</figure>`
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
/** Quadrant chart · 2-axis plot, labelled items, optional `focal`.
|
|
3717
|
+
* Items use {label, x, y} where x and y are 0..1 (0 = low, 1 = high).
|
|
3718
|
+
* Top-right quadrant is the "preferred" zone (accent-tinted background
|
|
3719
|
+
* + accent quad-label). Focal item renders solid accent; everything
|
|
3720
|
+
* else cascades from outline-ink to faint-muted by `tier` (1..4) if
|
|
3721
|
+
* provided, otherwise everyone shares the standard outline. */
|
|
3722
|
+
function renderKamiQuadrant(data) {
|
|
3723
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
3724
|
+
const xLabel = typeof data.xLabel === "string" ? data.xLabel.trim() : "";
|
|
3725
|
+
const yLabel = typeof data.yLabel === "string" ? data.yLabel.trim() : "";
|
|
3726
|
+
const q1 = typeof data.q1 === "string" ? data.q1.trim() : "";
|
|
3727
|
+
const q2 = typeof data.q2 === "string" ? data.q2.trim() : "";
|
|
3728
|
+
const q3 = typeof data.q3 === "string" ? data.q3.trim() : "";
|
|
3729
|
+
const q4 = typeof data.q4 === "string" ? data.q4.trim() : "";
|
|
3730
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
3731
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
3732
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
3733
|
+
? String(data.figNum) : "";
|
|
3734
|
+
const itemsRaw = Array.isArray(data.items) ? data.items : [];
|
|
3735
|
+
const items = [];
|
|
3736
|
+
for (const it of itemsRaw) {
|
|
3737
|
+
if (!it || typeof it !== "object") continue;
|
|
3738
|
+
const label = typeof it.label === "string" ? it.label.trim() : "";
|
|
3739
|
+
const x = Number(it.x);
|
|
3740
|
+
const y = Number(it.y);
|
|
3741
|
+
if (!label || !isFinite(x) || !isFinite(y)) continue;
|
|
3742
|
+
const tier = Number.isInteger(it.tier) ? Math.max(1, Math.min(4, it.tier)) : 0;
|
|
3743
|
+
items.push({ label, x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)), tier });
|
|
3744
|
+
if (items.length >= 16) break;
|
|
3745
|
+
}
|
|
3746
|
+
if (items.length < 2) {
|
|
3747
|
+
return `<div class="kami-chart-error" data-err="too-few-items"></div>`;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// Geometry · viewBox 820 × 540, plot area x∈[80, 740], y∈[60, 440].
|
|
3751
|
+
// y-up in data terms (y=0 bottom, y=1 top) → svg y flipped.
|
|
3752
|
+
const X0 = 80, X1 = 740, Y0 = 60, Y1 = 440;
|
|
3753
|
+
const CX = (X0 + X1) / 2, CY = (Y0 + Y1) / 2;
|
|
3754
|
+
const toSvgX = (x) => X0 + x * (X1 - X0);
|
|
3755
|
+
const toSvgY = (y) => Y1 - y * (Y1 - Y0);
|
|
3756
|
+
|
|
3757
|
+
// Quadrant text positions (offset from each quadrant centre)
|
|
3758
|
+
const quadEls = [
|
|
3759
|
+
// Q2 · top-left · high-y, low-x
|
|
3760
|
+
q2 ? `<text class="kc-q-quad-kicker" x="${X0 + 164}" y="${Y0 + 26}" text-anchor="middle">HIGH-Y · LOW-X</text>
|
|
3761
|
+
<text class="kc-q-quad-name" x="${X0 + 164}" y="${Y0 + 42}" text-anchor="middle">${escape(q2)}</text>` : "",
|
|
3762
|
+
// Q1 · top-right · high-y, high-x (PREFERRED)
|
|
3763
|
+
q1 ? `<text class="kc-q-quad-kicker kc-q-pref" x="${X1 - 164}" y="${Y0 + 26}" text-anchor="middle">HIGH-Y · HIGH-X</text>
|
|
3764
|
+
<text class="kc-q-quad-name kc-q-pref" x="${X1 - 164}" y="${Y0 + 42}" text-anchor="middle">${escape(q1)}</text>` : "",
|
|
3765
|
+
// Q3 · bottom-left · low-y, low-x
|
|
3766
|
+
q3 ? `<text class="kc-q-quad-kicker" x="${X0 + 164}" y="${Y1 - 28}" text-anchor="middle">LOW-Y · LOW-X</text>
|
|
3767
|
+
<text class="kc-q-quad-name" x="${X0 + 164}" y="${Y1 - 12}" text-anchor="middle">${escape(q3)}</text>` : "",
|
|
3768
|
+
// Q4 · bottom-right · low-y, high-x
|
|
3769
|
+
q4 ? `<text class="kc-q-quad-kicker" x="${X1 - 164}" y="${Y1 - 28}" text-anchor="middle">LOW-Y · HIGH-X</text>
|
|
3770
|
+
<text class="kc-q-quad-name" x="${X1 - 164}" y="${Y1 - 12}" text-anchor="middle">${escape(q4)}</text>` : "",
|
|
3771
|
+
].join("");
|
|
3772
|
+
|
|
3773
|
+
// Items · render in reverse z-order so focal lands on top
|
|
3774
|
+
const pointEls = items.map((it) => {
|
|
3775
|
+
const cx = Math.round(toSvgX(it.x));
|
|
3776
|
+
const cy = Math.round(toSvgY(it.y));
|
|
3777
|
+
const isFocal = focal && it.label === focal;
|
|
3778
|
+
let pointCls = "kc-point";
|
|
3779
|
+
let labelCls = "kc-point-label";
|
|
3780
|
+
let r = 5;
|
|
3781
|
+
if (isFocal) {
|
|
3782
|
+
pointCls = "kc-point-focal";
|
|
3783
|
+
labelCls = "kc-point-label kc-primary";
|
|
3784
|
+
r = 6.5;
|
|
3785
|
+
} else if (it.tier === 2) {
|
|
3786
|
+
pointCls = "kc-point-tinted";
|
|
3787
|
+
} else if (it.tier === 3) {
|
|
3788
|
+
pointCls = "kc-point";
|
|
3789
|
+
} else if (it.tier === 4) {
|
|
3790
|
+
pointCls = "kc-point-faint";
|
|
3791
|
+
labelCls = "kc-point-label kc-faint";
|
|
3792
|
+
r = 4;
|
|
3793
|
+
}
|
|
3794
|
+
// Place label to the right of the point; if point is in the right
|
|
3795
|
+
// half (cx > CX), pull label to the left so it doesn't overflow.
|
|
3796
|
+
const labelOnLeft = cx > X1 - 80;
|
|
3797
|
+
const lx = labelOnLeft ? cx - 10 : cx + 10;
|
|
3798
|
+
const ly = cy + 4;
|
|
3799
|
+
const anchor = labelOnLeft ? "end" : "start";
|
|
3800
|
+
return (
|
|
3801
|
+
`<circle class="${pointCls}" cx="${cx}" cy="${cy}" r="${r}"/>` +
|
|
3802
|
+
`<text class="${labelCls}" x="${lx}" y="${ly}" text-anchor="${anchor}">${escape(it.label)}</text>`
|
|
3803
|
+
);
|
|
3804
|
+
}).join("");
|
|
3805
|
+
|
|
3806
|
+
const xLabelEl = xLabel
|
|
3807
|
+
? `<text class="kc-q-axis-label" x="${CX}" y="${Y1 + 40}" text-anchor="middle">${escape(xLabel)}</text>`
|
|
3808
|
+
: "";
|
|
3809
|
+
const yLabelEl = yLabel
|
|
3810
|
+
? `<text class="kc-q-axis-label" x="30" y="${CY}" text-anchor="middle" transform="rotate(-90 30 ${CY})">${escape(yLabel)}</text>`
|
|
3811
|
+
: "";
|
|
3812
|
+
|
|
3813
|
+
const figHeader =
|
|
3814
|
+
`<div class="kc-figheader">` +
|
|
3815
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
3816
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
3817
|
+
`</div>`;
|
|
3818
|
+
|
|
3819
|
+
const captionEl = caption
|
|
3820
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
3821
|
+
: "";
|
|
3822
|
+
|
|
3823
|
+
return (
|
|
3824
|
+
`<figure class="kami-chart">` +
|
|
3825
|
+
figHeader +
|
|
3826
|
+
`<svg class="kc-svg" viewBox="0 0 820 500" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Quadrant chart")}">` +
|
|
3827
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
3828
|
+
`<rect class="kc-q-frame" x="${X0}" y="${Y0}" width="${X1 - X0}" height="${Y1 - Y0}"/>` +
|
|
3829
|
+
`<rect class="kc-q-tint" x="${CX}" y="${Y0}" width="${X1 - CX}" height="${CY - Y0}"/>` +
|
|
3830
|
+
`<line class="kc-q-axis" x1="${X0}" y1="${CY}" x2="${X1}" y2="${CY}"/>` +
|
|
3831
|
+
`<line class="kc-q-axis" x1="${CX}" y1="${Y0}" x2="${CX}" y2="${Y1}"/>` +
|
|
3832
|
+
`<line class="kc-q-arrow" x1="${X0}" y1="${Y1 + 16}" x2="${X1}" y2="${Y1 + 16}"/>` +
|
|
3833
|
+
`<path class="kc-q-arrow" d="M${X1 - 6},${Y1 + 12} L${X1},${Y1 + 16} L${X1 - 6},${Y1 + 20}"/>` +
|
|
3834
|
+
`<line class="kc-q-arrow" x1="60" y1="${Y1}" x2="60" y2="${Y0}"/>` +
|
|
3835
|
+
`<path class="kc-q-arrow" d="M56,${Y0 + 6} L60,${Y0} L64,${Y0 + 6}"/>` +
|
|
3836
|
+
quadEls +
|
|
3837
|
+
pointEls +
|
|
3838
|
+
xLabelEl +
|
|
3839
|
+
yLabelEl +
|
|
3840
|
+
`</svg>` +
|
|
3841
|
+
captionEl +
|
|
3842
|
+
`</figure>`
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
/** Timeline · 3–8 dated events on a horizontal axis. Events alternate
|
|
3847
|
+
* above / below the axis. `focal` (period of the load-bearing event)
|
|
3848
|
+
* renders the dot, connector, and card in the spine accent. */
|
|
3849
|
+
function renderKamiTimeline(data) {
|
|
3850
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
3851
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
3852
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
3853
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
3854
|
+
? String(data.figNum) : "";
|
|
3855
|
+
const pointsRaw = Array.isArray(data.points) ? data.points : [];
|
|
3856
|
+
const points = [];
|
|
3857
|
+
for (const p of pointsRaw) {
|
|
3858
|
+
if (!p || typeof p !== "object") continue;
|
|
3859
|
+
const period = typeof p.period === "string" ? p.period.trim() : "";
|
|
3860
|
+
const label = typeof p.label === "string" ? p.label.trim() : "";
|
|
3861
|
+
const description = typeof p.description === "string" ? p.description.trim() : "";
|
|
3862
|
+
if (!period || !label) continue;
|
|
3863
|
+
points.push({ period, label, description });
|
|
3864
|
+
if (points.length >= 8) break;
|
|
3865
|
+
}
|
|
3866
|
+
if (points.length < 3) {
|
|
3867
|
+
return `<div class="kami-chart-error" data-err="too-few-points"></div>`;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// Geometry · viewBox 960 × 380. Axis at y=200; cards above y∈[100,168],
|
|
3871
|
+
// below y∈[232,300]. Events evenly distributed across x∈[100,860].
|
|
3872
|
+
const X0 = 100, X1 = 860, AY = 200;
|
|
3873
|
+
const N = points.length;
|
|
3874
|
+
const step = (X1 - X0) / (N - 1);
|
|
3875
|
+
|
|
3876
|
+
const els = [];
|
|
3877
|
+
points.forEach((pt, i) => {
|
|
3878
|
+
const cx = Math.round(X0 + step * i);
|
|
3879
|
+
const above = i % 2 === 0;
|
|
3880
|
+
const isFocal = focal && pt.period === focal;
|
|
3881
|
+
|
|
3882
|
+
const dotCls = isFocal ? "kc-t-dot-focal" : "kc-t-dot";
|
|
3883
|
+
const conCls = isFocal ? "kc-t-connector-focal" : "kc-t-connector";
|
|
3884
|
+
const cardCls = isFocal ? "kc-t-card-focal" : "kc-t-card";
|
|
3885
|
+
const periodCls = isFocal ? "kc-t-period kc-t-period-focal" : "kc-t-period";
|
|
3886
|
+
const labelCls = isFocal ? "kc-t-label kc-t-label-focal" : "kc-t-label";
|
|
3887
|
+
const r = isFocal ? 6 : 5;
|
|
3888
|
+
|
|
3889
|
+
// Card geometry
|
|
3890
|
+
const cardW = 132, cardH = pt.description ? 56 : 40;
|
|
3891
|
+
const cardX = cx - cardW / 2;
|
|
3892
|
+
const cardY = above ? (AY - 32 - cardH) : (AY + 32);
|
|
3893
|
+
const conX = cx;
|
|
3894
|
+
const conY1 = above ? AY - 8 : AY + 8;
|
|
3895
|
+
const conY2 = above ? cardY + cardH : cardY;
|
|
3896
|
+
|
|
3897
|
+
const periodY = above ? cardY + 16 : cardY + 16;
|
|
3898
|
+
const labelY = above ? cardY + 32 : cardY + 32;
|
|
3899
|
+
const descY = above ? cardY + 48 : cardY + 48;
|
|
3900
|
+
|
|
3901
|
+
els.push(
|
|
3902
|
+
`<line class="${conCls}" x1="${conX}" y1="${conY1}" x2="${conX}" y2="${conY2}"/>` +
|
|
3903
|
+
`<circle class="${dotCls}" cx="${cx}" cy="${AY}" r="${r}"/>` +
|
|
3904
|
+
`<rect class="${cardCls}" x="${cardX}" y="${cardY}" width="${cardW}" height="${cardH}" rx="4"/>` +
|
|
3905
|
+
`<text class="${periodCls}" x="${cx}" y="${periodY}" text-anchor="middle">${escape(pt.period)}</text>` +
|
|
3906
|
+
`<text class="${labelCls}" x="${cx}" y="${labelY}" text-anchor="middle">${escape(pt.label)}</text>` +
|
|
3907
|
+
(pt.description
|
|
3908
|
+
? `<text class="kc-t-period" x="${cx}" y="${descY}" text-anchor="middle" style="text-transform:none;letter-spacing:0;font-size:9px">${escape(pt.description.length > 40 ? pt.description.slice(0, 39) + "…" : pt.description)}</text>`
|
|
3909
|
+
: ""),
|
|
3910
|
+
);
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
const figHeader =
|
|
3914
|
+
`<div class="kc-figheader">` +
|
|
3915
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
3916
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
3917
|
+
`</div>`;
|
|
3918
|
+
|
|
3919
|
+
const captionEl = caption
|
|
3920
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
3921
|
+
: "";
|
|
3922
|
+
|
|
3923
|
+
return (
|
|
3924
|
+
`<figure class="kami-chart">` +
|
|
3925
|
+
figHeader +
|
|
3926
|
+
`<svg class="kc-svg" viewBox="0 0 960 380" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Timeline")}">` +
|
|
3927
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
3928
|
+
`<line class="kc-t-axis" x1="${X0 - 40}" y1="${AY}" x2="${X1 + 40}" y2="${AY}"/>` +
|
|
3929
|
+
`<path class="kc-t-arrow" d="M${X1 + 34},${AY - 4} L${X1 + 40},${AY} L${X1 + 34},${AY + 4}"/>` +
|
|
3930
|
+
els.join("") +
|
|
3931
|
+
`</svg>` +
|
|
3932
|
+
captionEl +
|
|
3933
|
+
`</figure>`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
/** Donut chart · 2–6 slices summing to 100 (values may be raw, the
|
|
3938
|
+
* renderer normalises). Largest slice carries the accent; smaller
|
|
3939
|
+
* slices cascade down the neutral ramp. The hole shows the largest
|
|
3940
|
+
* slice's value + caption. */
|
|
3941
|
+
function renderKamiDonut(data) {
|
|
3942
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
3943
|
+
const centerLabel = typeof data.centerLabel === "string" ? data.centerLabel.trim() : "";
|
|
3944
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
3945
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
3946
|
+
? String(data.figNum) : "";
|
|
3947
|
+
const slicesRaw = Array.isArray(data.slices) ? data.slices : [];
|
|
3948
|
+
const slices = [];
|
|
3949
|
+
for (const s of slicesRaw) {
|
|
3950
|
+
if (!s || typeof s !== "object") continue;
|
|
3951
|
+
const label = typeof s.label === "string" ? s.label.trim() : "";
|
|
3952
|
+
const value = Number(s.value);
|
|
3953
|
+
if (!label || !isFinite(value) || value <= 0) continue;
|
|
3954
|
+
slices.push({ label, value });
|
|
3955
|
+
if (slices.length >= 6) break;
|
|
3956
|
+
}
|
|
3957
|
+
if (slices.length < 2) {
|
|
3958
|
+
return `<div class="kami-chart-error" data-err="too-few-slices"></div>`;
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// Sort largest-first so slice 1 = largest = accent
|
|
3962
|
+
slices.sort((a, b) => b.value - a.value);
|
|
3963
|
+
const total = slices.reduce((s, x) => s + x.value, 0);
|
|
3964
|
+
slices.forEach((s) => { s.pct = (s.value / total) * 100; });
|
|
3965
|
+
|
|
3966
|
+
// Geometry · viewBox 680 × 420 · donut centre (300, 200), R=136, r=76
|
|
3967
|
+
const CX = 300, CY = 200, R = 136, IR = 76;
|
|
3968
|
+
|
|
3969
|
+
function polar(r, deg) {
|
|
3970
|
+
const rad = (deg - 90) * Math.PI / 180; // -90 puts 0° at top
|
|
3971
|
+
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
|
|
3972
|
+
}
|
|
3973
|
+
function round1(v) { return Math.round(v * 10) / 10; }
|
|
3974
|
+
|
|
3975
|
+
let cursor = 0;
|
|
3976
|
+
const sliceEls = slices.map((s, i) => {
|
|
3977
|
+
const span = (s.value / total) * 360;
|
|
3978
|
+
const a0 = cursor;
|
|
3979
|
+
const a1 = cursor + span;
|
|
3980
|
+
cursor = a1;
|
|
3981
|
+
const large = span > 180 ? 1 : 0;
|
|
3982
|
+
const o0 = polar(R, a0);
|
|
3983
|
+
const o1 = polar(R, a1);
|
|
3984
|
+
const iEnd = polar(IR, a1);
|
|
3985
|
+
const iStart = polar(IR, a0);
|
|
3986
|
+
const cls = `kc-slice-${Math.min(6, i + 1)}`;
|
|
3987
|
+
const d = `M ${round1(o0.x)} ${round1(o0.y)} ` +
|
|
3988
|
+
`A ${R} ${R} 0 ${large} 1 ${round1(o1.x)} ${round1(o1.y)} ` +
|
|
3989
|
+
`L ${round1(iEnd.x)} ${round1(iEnd.y)} ` +
|
|
3990
|
+
`A ${IR} ${IR} 0 ${large} 0 ${round1(iStart.x)} ${round1(iStart.y)} Z`;
|
|
3991
|
+
return `<path class="${cls}" d="${d}"/>`;
|
|
3992
|
+
}).join("");
|
|
3993
|
+
|
|
3994
|
+
// Centre · largest slice's value + supplied label
|
|
3995
|
+
const lead = slices[0];
|
|
3996
|
+
const centerValue = Number.isInteger(lead.pct)
|
|
3997
|
+
? `${lead.pct}%`
|
|
3998
|
+
: `${lead.pct.toFixed(1)}%`;
|
|
3999
|
+
const centerLabelTxt = (centerLabel || lead.label).toUpperCase();
|
|
4000
|
+
|
|
4001
|
+
// Legend · vertical list right of donut. x=472 swatch, x=492 pct, x=524 label.
|
|
4002
|
+
const legendRow = (s, i) => {
|
|
4003
|
+
const y = 136 + i * 26;
|
|
4004
|
+
const cls = `kc-slice-${Math.min(6, i + 1)}`;
|
|
4005
|
+
const pctTxt = Number.isInteger(s.pct) ? `${s.pct}%` : `${s.pct.toFixed(1)}%`;
|
|
4006
|
+
const isPrimary = i === 0 ? " kc-primary" : "";
|
|
4007
|
+
return (
|
|
4008
|
+
`<rect class="kc-donut-legend-swatch ${cls}" x="472" y="${y}" width="12" height="12" rx="2"/>` +
|
|
4009
|
+
`<text class="kc-donut-legend-value${isPrimary}" x="492" y="${y + 10}">${escape(pctTxt)}</text>` +
|
|
4010
|
+
`<text class="kc-donut-legend-label" x="528" y="${y + 10}">${escape(s.label)}</text>`
|
|
4011
|
+
);
|
|
4012
|
+
};
|
|
4013
|
+
const legendEls = slices.map(legendRow).join("");
|
|
4014
|
+
const legendTopY = 124;
|
|
4015
|
+
const legendBotY = 136 + slices.length * 26;
|
|
4016
|
+
|
|
4017
|
+
const figHeader =
|
|
4018
|
+
`<div class="kc-figheader">` +
|
|
4019
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4020
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
4021
|
+
`</div>`;
|
|
4022
|
+
|
|
4023
|
+
const captionEl = caption
|
|
4024
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4025
|
+
: "";
|
|
4026
|
+
|
|
4027
|
+
return (
|
|
4028
|
+
`<figure class="kami-chart">` +
|
|
4029
|
+
figHeader +
|
|
4030
|
+
`<svg class="kc-svg" viewBox="0 0 680 400" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Donut chart")}">` +
|
|
4031
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4032
|
+
sliceEls +
|
|
4033
|
+
`<circle class="kc-donut-separator" cx="${CX}" cy="${CY}" r="${R}"/>` +
|
|
4034
|
+
`<circle class="kc-donut-separator" cx="${CX}" cy="${CY}" r="${IR}"/>` +
|
|
4035
|
+
`<circle class="kc-donut-hole" cx="${CX}" cy="${CY}" r="${IR - 1}"/>` +
|
|
4036
|
+
`<text class="kc-donut-center-value" x="${CX}" y="${CY - 7}" text-anchor="middle">${escape(centerValue)}</text>` +
|
|
4037
|
+
`<text class="kc-donut-center-label" x="${CX}" y="${CY + 14}" text-anchor="middle">${escape(centerLabelTxt)}</text>` +
|
|
4038
|
+
`<line class="kc-donut-legend-divider" x1="460" y1="${legendTopY}" x2="460" y2="${legendBotY}"/>` +
|
|
4039
|
+
legendEls +
|
|
4040
|
+
`</svg>` +
|
|
4041
|
+
captionEl +
|
|
4042
|
+
`</figure>`
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
/** Gantt chart · phases as horizontal bars over a shared time axis,
|
|
4047
|
+
* grouped by optional `section`. Replaces inline mermaid gantt blocks. */
|
|
4048
|
+
function renderKamiGantt(data) {
|
|
4049
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
4050
|
+
const unit = typeof data.unit === "string" ? data.unit.trim() : "";
|
|
4051
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
4052
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
4053
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
4054
|
+
? String(data.figNum) : "";
|
|
4055
|
+
const phasesRaw = Array.isArray(data.phases) ? data.phases : [];
|
|
4056
|
+
const phases = [];
|
|
4057
|
+
for (const p of phasesRaw) {
|
|
4058
|
+
if (!p || typeof p !== "object") continue;
|
|
4059
|
+
const label = typeof p.label === "string" ? p.label.trim() : "";
|
|
4060
|
+
const section = typeof p.section === "string" ? p.section.trim() : "";
|
|
4061
|
+
const start = Number(p.start);
|
|
4062
|
+
const end = Number(p.end);
|
|
4063
|
+
if (!label || !isFinite(start) || !isFinite(end) || end <= start) continue;
|
|
4064
|
+
phases.push({ label, section, start, end });
|
|
4065
|
+
if (phases.length >= 10) break;
|
|
4066
|
+
}
|
|
4067
|
+
if (phases.length < 2) {
|
|
4068
|
+
return `<div class="kami-chart-error" data-err="too-few-phases"></div>`;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// Group by section preserving order of first appearance.
|
|
4072
|
+
const sectionOrder = [];
|
|
4073
|
+
const sectionMap = new Map();
|
|
4074
|
+
phases.forEach((p) => {
|
|
4075
|
+
const key = p.section || "";
|
|
4076
|
+
if (!sectionMap.has(key)) {
|
|
4077
|
+
sectionOrder.push(key);
|
|
4078
|
+
sectionMap.set(key, []);
|
|
4079
|
+
}
|
|
4080
|
+
sectionMap.get(key).push(p);
|
|
4081
|
+
});
|
|
4082
|
+
const hasSections = sectionOrder.some((s) => s !== "");
|
|
4083
|
+
|
|
4084
|
+
// Geometry · viewBox: variable width, fixed by row count.
|
|
4085
|
+
// Left column for labels: 220 wide. Time axis: x∈[220, 920].
|
|
4086
|
+
const X0 = 220, X1 = 920;
|
|
4087
|
+
const rowH = 32;
|
|
4088
|
+
const sectionGap = hasSections ? 8 : 0;
|
|
4089
|
+
const sectionHeaderH = hasSections ? 22 : 0;
|
|
4090
|
+
let cursorY = 80; // start below the time axis
|
|
4091
|
+
const yByPhase = new Map();
|
|
4092
|
+
const sectionBlocks = [];
|
|
4093
|
+
sectionOrder.forEach((sec, sIdx) => {
|
|
4094
|
+
const tasks = sectionMap.get(sec);
|
|
4095
|
+
const headerY = cursorY;
|
|
4096
|
+
tasks.forEach((p) => {
|
|
4097
|
+
yByPhase.set(p, cursorY + sectionHeaderH);
|
|
4098
|
+
cursorY += rowH;
|
|
4099
|
+
});
|
|
4100
|
+
cursorY += sectionGap;
|
|
4101
|
+
sectionBlocks.push({ name: sec, headerY, taskCount: tasks.length });
|
|
4102
|
+
});
|
|
4103
|
+
cursorY += 20;
|
|
4104
|
+
const totalH = cursorY;
|
|
4105
|
+
const viewBoxW = 960;
|
|
4106
|
+
const viewBoxH = totalH + (caption ? 0 : 12);
|
|
4107
|
+
|
|
4108
|
+
// Time scale · axis goes 0..maxEnd
|
|
4109
|
+
const maxEnd = Math.max(...phases.map((p) => p.end), 0);
|
|
4110
|
+
const niceMax = kcNiceMax(maxEnd);
|
|
4111
|
+
const toX = (t) => X0 + (t / niceMax) * (X1 - X0);
|
|
4112
|
+
|
|
4113
|
+
// Time ticks · 5 evenly-spaced
|
|
4114
|
+
const tickCount = 5;
|
|
4115
|
+
const tickEls = [];
|
|
4116
|
+
for (let i = 0; i <= tickCount; i++) {
|
|
4117
|
+
const v = (niceMax / tickCount) * i;
|
|
4118
|
+
const x = toX(v);
|
|
4119
|
+
const ticked = `${kcFmt(v)}${unit ? " " + unit : ""}`;
|
|
4120
|
+
tickEls.push(`<text class="kc-g-time-tick" x="${x}" y="64" text-anchor="middle">${escape(ticked)}</text>`);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// Section headers + task rows
|
|
4124
|
+
let secCursor = 80;
|
|
4125
|
+
const sectionEls = [];
|
|
4126
|
+
const taskEls = [];
|
|
4127
|
+
sectionBlocks.forEach((blk, sIdx) => {
|
|
4128
|
+
const startY = secCursor + (hasSections ? sectionHeaderH : 0);
|
|
4129
|
+
const blockH = blk.taskCount * rowH;
|
|
4130
|
+
const endY = startY + blockH;
|
|
4131
|
+
if (sIdx % 2 === 0) {
|
|
4132
|
+
sectionEls.push(`<rect class="kc-g-section-bg" x="20" y="${startY}" width="${viewBoxW - 40}" height="${blockH}"/>`);
|
|
4133
|
+
}
|
|
4134
|
+
if (hasSections && blk.name) {
|
|
4135
|
+
sectionEls.push(`<text class="kc-g-section-label" x="40" y="${secCursor + 16}">${escape(blk.name)}</text>`);
|
|
4136
|
+
}
|
|
4137
|
+
secCursor = endY + sectionGap;
|
|
4138
|
+
});
|
|
4139
|
+
|
|
4140
|
+
phases.forEach((p) => {
|
|
4141
|
+
const y = yByPhase.get(p);
|
|
4142
|
+
const barY = y + 8;
|
|
4143
|
+
const barX = toX(p.start);
|
|
4144
|
+
const barW = toX(p.end) - barX;
|
|
4145
|
+
const isFocal = focal && p.label === focal;
|
|
4146
|
+
const barCls = isFocal ? "kc-g-bar kc-primary" : "kc-g-bar";
|
|
4147
|
+
const labelCls = isFocal ? "kc-g-task-label kc-primary" : "kc-g-task-label";
|
|
4148
|
+
const labelTxt = p.label.length > 24 ? p.label.slice(0, 23) + "…" : p.label;
|
|
4149
|
+
// Show bar duration inside the bar if it fits, else outside.
|
|
4150
|
+
const span = p.end - p.start;
|
|
4151
|
+
const spanTxt = `${kcFmt(span)}${unit ? unit.slice(0, 1) : ""}`;
|
|
4152
|
+
const spanX = barW > 36 ? barX + barW / 2 : barX + barW + 6;
|
|
4153
|
+
const spanCls = barW > 36 ? "kc-g-bar-label" : "kc-g-bar-label kc-outside";
|
|
4154
|
+
const spanAnchor = barW > 36 ? "middle" : "start";
|
|
4155
|
+
taskEls.push(
|
|
4156
|
+
`<text class="${labelCls}" x="200" y="${barY + 13}" text-anchor="end">${escape(labelTxt)}</text>` +
|
|
4157
|
+
`<rect class="${barCls}" x="${barX}" y="${barY}" width="${Math.max(2, barW)}" height="16" rx="2"/>` +
|
|
4158
|
+
`<text class="${spanCls}" x="${spanX}" y="${barY + 11}" text-anchor="${spanAnchor}">${escape(spanTxt)}</text>` +
|
|
4159
|
+
`<line class="kc-g-row-rule" x1="20" y1="${y + rowH}" x2="${viewBoxW - 20}" y2="${y + rowH}"/>`,
|
|
4160
|
+
);
|
|
4161
|
+
});
|
|
4162
|
+
|
|
4163
|
+
const figHeader =
|
|
4164
|
+
`<div class="kc-figheader">` +
|
|
4165
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4166
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
4167
|
+
`</div>`;
|
|
4168
|
+
|
|
4169
|
+
const captionEl = caption
|
|
4170
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4171
|
+
: "";
|
|
4172
|
+
|
|
4173
|
+
return (
|
|
4174
|
+
`<figure class="kami-chart">` +
|
|
4175
|
+
figHeader +
|
|
4176
|
+
`<svg class="kc-svg" viewBox="0 0 ${viewBoxW} ${viewBoxH}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Gantt chart")}">` +
|
|
4177
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4178
|
+
sectionEls.join("") +
|
|
4179
|
+
`<line class="kc-g-time-axis" x1="${X0}" y1="72" x2="${X1}" y2="72"/>` +
|
|
4180
|
+
tickEls.join("") +
|
|
4181
|
+
taskEls.join("") +
|
|
4182
|
+
`</svg>` +
|
|
4183
|
+
captionEl +
|
|
4184
|
+
`</figure>`
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
/** Tree (mindmap) · 1 root + 2–6 branches × 0–4 leaves per branch.
|
|
4189
|
+
* Replaces inline mermaid mindmap blocks. Layout: root centred on
|
|
4190
|
+
* top, branches spread evenly on a horizontal band, leaves drop
|
|
4191
|
+
* beneath each branch with even horizontal spacing. */
|
|
4192
|
+
function renderKamiTree(data) {
|
|
4193
|
+
const rootRaw = typeof data.root === "string" ? data.root.trim() : "";
|
|
4194
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
4195
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
4196
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
4197
|
+
? String(data.figNum) : "";
|
|
4198
|
+
const branchesRaw = Array.isArray(data.branches) ? data.branches : [];
|
|
4199
|
+
const branches = [];
|
|
4200
|
+
for (const b of branchesRaw) {
|
|
4201
|
+
if (!b || typeof b !== "object") continue;
|
|
4202
|
+
const label = typeof b.label === "string" ? b.label.trim() : "";
|
|
4203
|
+
if (!label) continue;
|
|
4204
|
+
const leaves = Array.isArray(b.leaves) ? b.leaves
|
|
4205
|
+
.map((l) => typeof l === "string" ? l.trim() : "")
|
|
4206
|
+
.filter(Boolean)
|
|
4207
|
+
.slice(0, 4) : [];
|
|
4208
|
+
branches.push({ label, leaves });
|
|
4209
|
+
if (branches.length >= 6) break;
|
|
4210
|
+
}
|
|
4211
|
+
if (!rootRaw || branches.length < 2) {
|
|
4212
|
+
return `<div class="kami-chart-error" data-err="too-small-tree"></div>`;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
// Layout · viewBox 960 × 460.
|
|
4216
|
+
const VBW = 960, VBH = 460;
|
|
4217
|
+
const ROOT_Y = 64, ROOT_H = 48, ROOT_W = 168;
|
|
4218
|
+
const BRANCH_Y = 160, BRANCH_H = 48, BRANCH_W = 144;
|
|
4219
|
+
const LEAF_Y = 290, LEAF_H = 48, LEAF_W = 128;
|
|
4220
|
+
const N = branches.length;
|
|
4221
|
+
const totalLeafCount = branches.reduce((s, b) => s + b.leaves.length, 0);
|
|
4222
|
+
const hasLeaves = totalLeafCount > 0;
|
|
4223
|
+
const SVG_H = hasLeaves ? 380 : 240;
|
|
4224
|
+
|
|
4225
|
+
// Spread branches evenly across plot width [80, 880]
|
|
4226
|
+
const plotW = VBW - 160;
|
|
4227
|
+
const branchX = (i) => Math.round(80 + (plotW / N) * i + (plotW / N) / 2);
|
|
4228
|
+
|
|
4229
|
+
// Root centre
|
|
4230
|
+
const rootCX = VBW / 2;
|
|
4231
|
+
|
|
4232
|
+
const els = [];
|
|
4233
|
+
|
|
4234
|
+
// Connectors · root → each branch top
|
|
4235
|
+
branches.forEach((b, i) => {
|
|
4236
|
+
const isFocal = focal && b.label === focal;
|
|
4237
|
+
const conCls = isFocal ? "kc-tree-connector-focal" : "kc-tree-connector";
|
|
4238
|
+
const chevCls = isFocal ? "kc-tree-chevron-focal" : "kc-tree-chevron";
|
|
4239
|
+
const bx = branchX(i);
|
|
4240
|
+
const midY = (ROOT_Y + ROOT_H + BRANCH_Y) / 2;
|
|
4241
|
+
// Path: from root bottom-center → drop to midY → horizontal to branch x → drop to branch top
|
|
4242
|
+
els.push(
|
|
4243
|
+
`<path class="${conCls}" d="M ${rootCX} ${ROOT_Y + ROOT_H} L ${rootCX} ${midY} L ${bx} ${midY} L ${bx} ${BRANCH_Y}"/>` +
|
|
4244
|
+
`<path class="${chevCls}" d="M ${bx - 5} ${BRANCH_Y - 4} L ${bx} ${BRANCH_Y} L ${bx + 5} ${BRANCH_Y - 4}"/>`,
|
|
4245
|
+
);
|
|
4246
|
+
});
|
|
4247
|
+
|
|
4248
|
+
// Branch nodes
|
|
4249
|
+
branches.forEach((b, i) => {
|
|
4250
|
+
const isFocal = focal && b.label === focal;
|
|
4251
|
+
const bx = branchX(i);
|
|
4252
|
+
const rectCls = isFocal ? "kc-tree-branch-focal" : "kc-tree-branch";
|
|
4253
|
+
const kickerCls = isFocal ? "kc-tree-kicker kc-tree-kicker-focal" : "kc-tree-kicker";
|
|
4254
|
+
const kickerTxt = isFocal ? "FOCAL" : `BRANCH ${String(i + 1).padStart(2, "0")}`;
|
|
4255
|
+
const labelTxt = b.label.length > 18 ? b.label.slice(0, 17) + "…" : b.label;
|
|
4256
|
+
els.push(
|
|
4257
|
+
`<rect class="${rectCls}" x="${bx - BRANCH_W / 2}" y="${BRANCH_Y}" width="${BRANCH_W}" height="${BRANCH_H}" rx="6"/>` +
|
|
4258
|
+
`<text class="${kickerCls}" x="${bx - BRANCH_W / 2 + 16}" y="${BRANCH_Y + 16}">${escape(kickerTxt)}</text>` +
|
|
4259
|
+
`<text class="kc-tree-text" x="${bx}" y="${BRANCH_Y + 34}" text-anchor="middle">${escape(labelTxt)}</text>`,
|
|
4260
|
+
);
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
// Leaves per branch · spread under each branch
|
|
4264
|
+
if (hasLeaves) {
|
|
4265
|
+
branches.forEach((b, i) => {
|
|
4266
|
+
if (b.leaves.length === 0) return;
|
|
4267
|
+
const isFocalBranch = focal && b.label === focal;
|
|
4268
|
+
const conCls = isFocalBranch ? "kc-tree-connector-focal" : "kc-tree-connector";
|
|
4269
|
+
const chevCls = isFocalBranch ? "kc-tree-chevron-focal" : "kc-tree-chevron";
|
|
4270
|
+
const leafCls = isFocalBranch ? "kc-tree-leaf-focal" : "kc-tree-leaf";
|
|
4271
|
+
const bx = branchX(i);
|
|
4272
|
+
const slotW = Math.min(LEAF_W + 8, (plotW / N) - 4);
|
|
4273
|
+
const leafCount = b.leaves.length;
|
|
4274
|
+
const totalSpan = slotW * leafCount;
|
|
4275
|
+
const startX = bx - totalSpan / 2 + slotW / 2;
|
|
4276
|
+
const midY = (BRANCH_Y + BRANCH_H + LEAF_Y) / 2;
|
|
4277
|
+
|
|
4278
|
+
b.leaves.forEach((leaf, k) => {
|
|
4279
|
+
const lx = Math.round(startX + slotW * k);
|
|
4280
|
+
// Connector from branch bottom → mid → leaf top
|
|
4281
|
+
els.push(
|
|
4282
|
+
`<path class="${conCls}" d="M ${bx} ${BRANCH_Y + BRANCH_H} L ${bx} ${midY} L ${lx} ${midY} L ${lx} ${LEAF_Y}"/>` +
|
|
4283
|
+
`<path class="${chevCls}" d="M ${lx - 5} ${LEAF_Y - 4} L ${lx} ${LEAF_Y} L ${lx + 5} ${LEAF_Y - 4}"/>`,
|
|
4284
|
+
);
|
|
4285
|
+
const leafTxt = leaf.length > 16 ? leaf.slice(0, 15) + "…" : leaf;
|
|
4286
|
+
// Leaf width based on slot
|
|
4287
|
+
const lw = Math.min(LEAF_W, slotW - 8);
|
|
4288
|
+
els.push(
|
|
4289
|
+
`<rect class="${leafCls}" x="${lx - lw / 2}" y="${LEAF_Y}" width="${lw}" height="${LEAF_H}" rx="6"/>` +
|
|
4290
|
+
`<text class="kc-tree-text" x="${lx}" y="${LEAF_Y + 28}" text-anchor="middle" style="font-size:10px">${escape(leafTxt)}</text>`,
|
|
4291
|
+
);
|
|
4292
|
+
});
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// Root node (drawn last so it sits on top of any connectors at the top)
|
|
4297
|
+
const rootLabel = rootRaw.length > 22 ? rootRaw.slice(0, 21) + "…" : rootRaw;
|
|
4298
|
+
els.unshift(
|
|
4299
|
+
`<rect class="kc-tree-root" x="${rootCX - ROOT_W / 2}" y="${ROOT_Y}" width="${ROOT_W}" height="${ROOT_H}" rx="6"/>` +
|
|
4300
|
+
`<text class="kc-tree-kicker" x="${rootCX - ROOT_W / 2 + 16}" y="${ROOT_Y + 16}">ROOT</text>` +
|
|
4301
|
+
`<text class="kc-tree-text" x="${rootCX}" y="${ROOT_Y + 34}" text-anchor="middle">${escape(rootLabel)}</text>`,
|
|
4302
|
+
);
|
|
4303
|
+
|
|
4304
|
+
const figHeader =
|
|
4305
|
+
`<div class="kc-figheader">` +
|
|
4306
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4307
|
+
(data.title ? `<span class="kc-figtitle">${escape(String(data.title).trim())}</span>` : "") +
|
|
4308
|
+
`</div>`;
|
|
4309
|
+
|
|
4310
|
+
const captionEl = caption
|
|
4311
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4312
|
+
: "";
|
|
4313
|
+
|
|
4314
|
+
return (
|
|
4315
|
+
`<figure class="kami-chart">` +
|
|
4316
|
+
figHeader +
|
|
4317
|
+
`<svg class="kc-svg" viewBox="0 0 ${VBW} ${SVG_H}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(data.title || "Tree")}">` +
|
|
4318
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4319
|
+
els.join("") +
|
|
4320
|
+
`</svg>` +
|
|
4321
|
+
captionEl +
|
|
4322
|
+
`</figure>`
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
/** Flowchart · two layout templates:
|
|
4327
|
+
* · linear-v · 2–5 nodes top-to-bottom, single column with arrows
|
|
4328
|
+
* · y-decision · root (top) → 2 branches (left/right) → optional join.
|
|
4329
|
+
* Replaces inline mermaid flowchart blocks. */
|
|
4330
|
+
function renderKamiFlowchart(data) {
|
|
4331
|
+
const layout = typeof data.layout === "string" ? data.layout.trim() : "linear-v";
|
|
4332
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
4333
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
4334
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
4335
|
+
? String(data.figNum) : "";
|
|
4336
|
+
const nodesRaw = Array.isArray(data.nodes) ? data.nodes : [];
|
|
4337
|
+
const nodes = [];
|
|
4338
|
+
for (const n of nodesRaw) {
|
|
4339
|
+
if (!n || typeof n !== "object") continue;
|
|
4340
|
+
const label = typeof n.label === "string" ? n.label.trim() : "";
|
|
4341
|
+
if (!label) continue;
|
|
4342
|
+
const kind = typeof n.kind === "string" ? n.kind.trim() : "step";
|
|
4343
|
+
const hint = typeof n.hint === "string" ? n.hint.trim() : "";
|
|
4344
|
+
nodes.push({ label, kind, hint });
|
|
4345
|
+
}
|
|
4346
|
+
if (nodes.length < 2) {
|
|
4347
|
+
return `<div class="kami-chart-error" data-err="too-few-nodes"></div>`;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
if (layout === "y-decision") {
|
|
4351
|
+
return renderKamiFlowchartYDecision(nodes, focal, caption, figNum, data);
|
|
4352
|
+
}
|
|
4353
|
+
return renderKamiFlowchartLinearV(nodes, focal, caption, figNum, data);
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
/** Render a flowchart node (returns SVG string). Kind controls shape:
|
|
4357
|
+
* · start / end · rounded pill, paper-soft fill
|
|
4358
|
+
* · decision · diamond, accent border + paper-deep fill
|
|
4359
|
+
* · outcome · square box, paper-deep fill, muted border (the
|
|
4360
|
+
* "deferred" branch reads as receded)
|
|
4361
|
+
* · step (default) · square box, paper fill, ink border
|
|
4362
|
+
* Focal nodes get the accent treatment regardless of kind. */
|
|
4363
|
+
function kcFlowNode(opts) {
|
|
4364
|
+
const { x, y, w, h, kind, label, hint, isFocal } = opts;
|
|
4365
|
+
const labelClipped = label.length > 18 ? label.slice(0, 17) + "…" : label;
|
|
4366
|
+
const cx = x + w / 2;
|
|
4367
|
+
const cy = y + h / 2;
|
|
4368
|
+
|
|
4369
|
+
if (kind === "decision") {
|
|
4370
|
+
// Diamond · 6-point polygon
|
|
4371
|
+
const points = [
|
|
4372
|
+
`${cx},${y}`,
|
|
4373
|
+
`${x + w},${cy - h * 0.18}`,
|
|
4374
|
+
`${x + w},${cy + h * 0.18}`,
|
|
4375
|
+
`${cx},${y + h}`,
|
|
4376
|
+
`${x},${cy + h * 0.18}`,
|
|
4377
|
+
`${x},${cy - h * 0.18}`,
|
|
4378
|
+
].join(" ");
|
|
4379
|
+
const cls = isFocal ? "kc-fl-node-focal" : "kc-fl-node";
|
|
4380
|
+
const kickerCls = isFocal ? "kc-fl-kicker kc-fl-kicker-focal" : "kc-fl-kicker";
|
|
4381
|
+
return (
|
|
4382
|
+
`<polygon class="${cls}" points="${points}"/>` +
|
|
4383
|
+
`<text class="${kickerCls}" x="${cx}" y="${cy - 8}" text-anchor="middle">DECISION</text>` +
|
|
4384
|
+
`<text class="kc-fl-text" x="${cx}" y="${cy + 6}" text-anchor="middle">${escape(labelClipped)}</text>` +
|
|
4385
|
+
(hint ? `<text class="kc-fl-edge-label" x="${cx}" y="${cy + 18}" text-anchor="middle">${escape(hint.toUpperCase())}</text>` : "")
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
if (kind === "start" || kind === "end") {
|
|
4390
|
+
// Pill · rounded rect, paper-soft
|
|
4391
|
+
const cls = "kc-fl-node-pill";
|
|
4392
|
+
const labelTxt = label.length > 16 ? label.slice(0, 15) + "…" : label;
|
|
4393
|
+
return (
|
|
4394
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${w}" height="${h}" rx="${h / 2}"/>` +
|
|
4395
|
+
`<text class="kc-fl-text" x="${cx}" y="${cy + 4}" text-anchor="middle">${escape(labelTxt)}</text>`
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
if (kind === "outcome") {
|
|
4400
|
+
// Square, faint
|
|
4401
|
+
const cls = isFocal ? "kc-fl-node-focal" : "kc-fl-node-faint";
|
|
4402
|
+
const kickerCls = isFocal ? "kc-fl-kicker kc-fl-kicker-focal" : "kc-fl-kicker";
|
|
4403
|
+
const kicker = (opts.kicker || "OUTCOME").toUpperCase();
|
|
4404
|
+
const textCls = isFocal ? "kc-fl-text" : "kc-fl-text kc-fl-text-faint";
|
|
4405
|
+
return (
|
|
4406
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${w}" height="${h}" rx="6"/>` +
|
|
4407
|
+
`<text class="${kickerCls}" x="${x + 16}" y="${y + 16}">${escape(kicker)}</text>` +
|
|
4408
|
+
`<text class="${textCls}" x="${cx}" y="${cy + 8}" text-anchor="middle">${escape(labelClipped)}</text>` +
|
|
4409
|
+
(hint ? `<text class="kc-fl-kicker" x="${cx}" y="${y + h - 8}" text-anchor="middle">${escape(hint)}</text>` : "")
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
// step (default) · square box
|
|
4414
|
+
const cls = isFocal ? "kc-fl-node-focal" : "kc-fl-node";
|
|
4415
|
+
const kickerCls = isFocal ? "kc-fl-kicker kc-fl-kicker-focal" : "kc-fl-kicker";
|
|
4416
|
+
const kicker = (opts.kicker || "STEP").toUpperCase();
|
|
4417
|
+
return (
|
|
4418
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${w}" height="${h}" rx="6"/>` +
|
|
4419
|
+
`<text class="${kickerCls}" x="${x + 16}" y="${y + 16}">${escape(kicker)}</text>` +
|
|
4420
|
+
`<text class="kc-fl-text" x="${cx}" y="${cy + 8}" text-anchor="middle">${escape(labelClipped)}</text>` +
|
|
4421
|
+
(hint ? `<text class="kc-fl-kicker" x="${cx}" y="${y + h - 8}" text-anchor="middle">${escape(hint)}</text>` : "")
|
|
4422
|
+
);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
function renderKamiFlowchartLinearV(nodes, focal, caption, figNum, data) {
|
|
4426
|
+
// Up to 5 nodes vertical · viewBox 600 × dynamic
|
|
4427
|
+
const N = Math.min(nodes.length, 5);
|
|
4428
|
+
const arr = nodes.slice(0, N);
|
|
4429
|
+
const W = 600;
|
|
4430
|
+
const nodeW = 220, nodeH = 60;
|
|
4431
|
+
const gap = 36;
|
|
4432
|
+
const startY = 30;
|
|
4433
|
+
const totalH = startY + N * nodeH + (N - 1) * gap + 30;
|
|
4434
|
+
|
|
4435
|
+
const cx = W / 2;
|
|
4436
|
+
const els = [];
|
|
4437
|
+
|
|
4438
|
+
arr.forEach((n, i) => {
|
|
4439
|
+
const isFocal = focal && n.label === focal;
|
|
4440
|
+
const y = startY + i * (nodeH + gap);
|
|
4441
|
+
const x = cx - nodeW / 2;
|
|
4442
|
+
// Connector from previous to this
|
|
4443
|
+
if (i > 0) {
|
|
4444
|
+
const prevY = startY + (i - 1) * (nodeH + gap) + nodeH;
|
|
4445
|
+
const arrowCls = (focal && (arr[i - 1].label === focal || n.label === focal))
|
|
4446
|
+
? "kc-fl-arrow-focal" : "kc-fl-arrow";
|
|
4447
|
+
const headCls = (focal && (arr[i - 1].label === focal || n.label === focal))
|
|
4448
|
+
? "kc-fl-arrowhead-focal" : "kc-fl-arrowhead";
|
|
4449
|
+
els.push(
|
|
4450
|
+
`<line class="${arrowCls}" x1="${cx}" y1="${prevY}" x2="${cx}" y2="${y}"/>` +
|
|
4451
|
+
`<path class="${headCls}" d="M ${cx - 5} ${y - 6} L ${cx} ${y} L ${cx + 5} ${y - 6}"/>`,
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
4454
|
+
els.push(kcFlowNode({
|
|
4455
|
+
x, y, w: nodeW, h: nodeH,
|
|
4456
|
+
kind: n.kind, label: n.label, hint: n.hint, isFocal,
|
|
4457
|
+
kicker: n.kind === "start" ? "" : (n.kind === "end" ? "" : `STEP ${String(i + 1).padStart(2, "0")}`),
|
|
4458
|
+
}));
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
const figHeader =
|
|
4462
|
+
`<div class="kc-figheader">` +
|
|
4463
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4464
|
+
(data.title ? `<span class="kc-figtitle">${escape(String(data.title).trim())}</span>` : "") +
|
|
4465
|
+
`</div>`;
|
|
4466
|
+
|
|
4467
|
+
const captionEl = caption
|
|
4468
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4469
|
+
: "";
|
|
4470
|
+
|
|
4471
|
+
return (
|
|
4472
|
+
`<figure class="kami-chart">` +
|
|
4473
|
+
figHeader +
|
|
4474
|
+
`<svg class="kc-svg" viewBox="0 0 ${W} ${totalH}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(data.title || "Flowchart")}">` +
|
|
4475
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4476
|
+
els.join("") +
|
|
4477
|
+
`</svg>` +
|
|
4478
|
+
captionEl +
|
|
4479
|
+
`</figure>`
|
|
4480
|
+
);
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
function renderKamiFlowchartYDecision(nodes, focal, caption, figNum, data) {
|
|
4484
|
+
// y-decision · expects nodes in this canonical order:
|
|
4485
|
+
// [0] root (decision diamond)
|
|
4486
|
+
// [1] left branch outcome
|
|
4487
|
+
// [2] right branch outcome
|
|
4488
|
+
// [3] optional join / end
|
|
4489
|
+
// Edge labels come from each branch node's `hint` field (e.g. "YES", "NO").
|
|
4490
|
+
const arr = nodes.slice(0, 4);
|
|
4491
|
+
if (arr.length < 3) {
|
|
4492
|
+
return `<div class="kami-chart-error" data-err="y-decision needs 3-4 nodes"></div>`;
|
|
4493
|
+
}
|
|
4494
|
+
const hasJoin = arr.length === 4;
|
|
4495
|
+
const W = 720;
|
|
4496
|
+
const totalH = hasJoin ? 380 : 280;
|
|
4497
|
+
const rootY = 30;
|
|
4498
|
+
const branchY = 160;
|
|
4499
|
+
const joinY = 280;
|
|
4500
|
+
const rootW = 200, rootH = 80;
|
|
4501
|
+
const branchW = 180, branchH = 70;
|
|
4502
|
+
const joinW = 140, joinH = 50;
|
|
4503
|
+
|
|
4504
|
+
const cx = W / 2;
|
|
4505
|
+
const leftCX = cx - 180;
|
|
4506
|
+
const rightCX = cx + 180;
|
|
4507
|
+
|
|
4508
|
+
const root = arr[0];
|
|
4509
|
+
const left = arr[1];
|
|
4510
|
+
const right = arr[2];
|
|
4511
|
+
const join = arr[3];
|
|
4512
|
+
|
|
4513
|
+
const isRootFocal = focal && root.label === focal;
|
|
4514
|
+
const isLeftFocal = focal && left.label === focal;
|
|
4515
|
+
const isRightFocal = focal && right.label === focal;
|
|
4516
|
+
const isJoinFocal = hasJoin && focal && join.label === focal;
|
|
4517
|
+
|
|
4518
|
+
const els = [];
|
|
4519
|
+
|
|
4520
|
+
// Root → Left branch
|
|
4521
|
+
const leftArrowCls = (isRootFocal || isLeftFocal) ? "kc-fl-arrow-focal" : "kc-fl-arrow";
|
|
4522
|
+
const leftHeadCls = (isRootFocal || isLeftFocal) ? "kc-fl-arrowhead-focal" : "kc-fl-arrowhead";
|
|
4523
|
+
const leftPath = `M ${cx - 30} ${rootY + rootH - 10} L ${leftCX} ${rootY + rootH + 30} L ${leftCX} ${branchY}`;
|
|
4524
|
+
els.push(`<path class="${leftArrowCls}" d="${leftPath}"/>`);
|
|
4525
|
+
els.push(`<path class="${leftHeadCls}" d="M ${leftCX - 5} ${branchY - 6} L ${leftCX} ${branchY} L ${leftCX + 5} ${branchY - 6}"/>`);
|
|
4526
|
+
// Edge label for left branch (from left.hint)
|
|
4527
|
+
if (left.hint) {
|
|
4528
|
+
const labelMidX = leftCX + 40;
|
|
4529
|
+
const labelMidY = (rootY + rootH + branchY) / 2 - 4;
|
|
4530
|
+
const labelTxt = left.hint.toUpperCase();
|
|
4531
|
+
const labelW = labelTxt.length * 6 + 14;
|
|
4532
|
+
const labelCls = isLeftFocal ? "kc-fl-edge-label kc-primary" : "kc-fl-edge-label";
|
|
4533
|
+
els.push(
|
|
4534
|
+
`<rect class="kc-fl-edge-label-bg" x="${labelMidX - labelW / 2}" y="${labelMidY - 8}" width="${labelW}" height="14" rx="2"/>` +
|
|
4535
|
+
`<text class="${labelCls}" x="${labelMidX}" y="${labelMidY + 3}" text-anchor="middle">${escape(labelTxt)}</text>`,
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
// Root → Right branch
|
|
4540
|
+
const rightArrowCls = (isRootFocal || isRightFocal) ? "kc-fl-arrow-focal" : "kc-fl-arrow";
|
|
4541
|
+
const rightHeadCls = (isRootFocal || isRightFocal) ? "kc-fl-arrowhead-focal" : "kc-fl-arrowhead";
|
|
4542
|
+
const rightPath = `M ${cx + 30} ${rootY + rootH - 10} L ${rightCX} ${rootY + rootH + 30} L ${rightCX} ${branchY}`;
|
|
4543
|
+
els.push(`<path class="${rightArrowCls}" d="${rightPath}"/>`);
|
|
4544
|
+
els.push(`<path class="${rightHeadCls}" d="M ${rightCX - 5} ${branchY - 6} L ${rightCX} ${branchY} L ${rightCX + 5} ${branchY - 6}"/>`);
|
|
4545
|
+
if (right.hint) {
|
|
4546
|
+
const labelMidX = rightCX - 40;
|
|
4547
|
+
const labelMidY = (rootY + rootH + branchY) / 2 - 4;
|
|
4548
|
+
const labelTxt = right.hint.toUpperCase();
|
|
4549
|
+
const labelW = labelTxt.length * 6 + 14;
|
|
4550
|
+
const labelCls = isRightFocal ? "kc-fl-edge-label kc-primary" : "kc-fl-edge-label";
|
|
4551
|
+
els.push(
|
|
4552
|
+
`<rect class="kc-fl-edge-label-bg" x="${labelMidX - labelW / 2}" y="${labelMidY - 8}" width="${labelW}" height="14" rx="2"/>` +
|
|
4553
|
+
`<text class="${labelCls}" x="${labelMidX}" y="${labelMidY + 3}" text-anchor="middle">${escape(labelTxt)}</text>`,
|
|
4554
|
+
);
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
// Branches → join (if exists)
|
|
4558
|
+
if (hasJoin) {
|
|
4559
|
+
const joinArrowCls = (isJoinFocal) ? "kc-fl-arrow-focal" : "kc-fl-arrow";
|
|
4560
|
+
const joinHeadCls = (isJoinFocal) ? "kc-fl-arrowhead-focal" : "kc-fl-arrowhead";
|
|
4561
|
+
const leftJoinPath = `M ${leftCX} ${branchY + branchH} L ${leftCX} ${joinY + joinH / 2 - 20} L ${cx - joinW / 2} ${joinY + joinH / 2 - 8}`;
|
|
4562
|
+
const rightJoinPath = `M ${rightCX} ${branchY + branchH} L ${rightCX} ${joinY + joinH / 2 - 20} L ${cx + joinW / 2} ${joinY + joinH / 2 - 8}`;
|
|
4563
|
+
els.push(`<path class="${joinArrowCls}" d="${leftJoinPath}"/>`);
|
|
4564
|
+
els.push(`<path class="${joinHeadCls}" d="M ${cx - joinW / 2 - 5} ${joinY + joinH / 2 - 14} L ${cx - joinW / 2} ${joinY + joinH / 2 - 8} L ${cx - joinW / 2 - 5} ${joinY + joinH / 2 - 2}"/>`);
|
|
4565
|
+
els.push(`<path class="${joinArrowCls}" d="${rightJoinPath}"/>`);
|
|
4566
|
+
els.push(`<path class="${joinHeadCls}" d="M ${cx + joinW / 2 + 5} ${joinY + joinH / 2 - 14} L ${cx + joinW / 2} ${joinY + joinH / 2 - 8} L ${cx + joinW / 2 + 5} ${joinY + joinH / 2 - 2}"/>`);
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
// Root node (decision diamond by default; respect explicit kind)
|
|
4570
|
+
const rootKind = root.kind || "decision";
|
|
4571
|
+
els.push(kcFlowNode({
|
|
4572
|
+
x: cx - rootW / 2, y: rootY, w: rootW, h: rootH,
|
|
4573
|
+
kind: rootKind, label: root.label, hint: root.hint, isFocal: isRootFocal,
|
|
4574
|
+
}));
|
|
4575
|
+
|
|
4576
|
+
// Left branch (outcome by default)
|
|
4577
|
+
els.push(kcFlowNode({
|
|
4578
|
+
x: leftCX - branchW / 2, y: branchY, w: branchW, h: branchH,
|
|
4579
|
+
kind: left.kind || "outcome", label: left.label, hint: left.hint, isFocal: isLeftFocal,
|
|
4580
|
+
kicker: "OUTCOME A",
|
|
4581
|
+
}));
|
|
4582
|
+
|
|
4583
|
+
// Right branch (outcome by default)
|
|
4584
|
+
els.push(kcFlowNode({
|
|
4585
|
+
x: rightCX - branchW / 2, y: branchY, w: branchW, h: branchH,
|
|
4586
|
+
kind: right.kind || "outcome", label: right.label, hint: right.hint, isFocal: isRightFocal,
|
|
4587
|
+
kicker: "OUTCOME B",
|
|
4588
|
+
}));
|
|
4589
|
+
|
|
4590
|
+
// Join (end pill by default)
|
|
4591
|
+
if (hasJoin) {
|
|
4592
|
+
els.push(kcFlowNode({
|
|
4593
|
+
x: cx - joinW / 2, y: joinY, w: joinW, h: joinH,
|
|
4594
|
+
kind: join.kind || "end", label: join.label, hint: join.hint, isFocal: isJoinFocal,
|
|
4595
|
+
}));
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
const figHeader =
|
|
4599
|
+
`<div class="kc-figheader">` +
|
|
4600
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4601
|
+
(data.title ? `<span class="kc-figtitle">${escape(String(data.title).trim())}</span>` : "") +
|
|
4602
|
+
`</div>`;
|
|
4603
|
+
|
|
4604
|
+
const captionEl = caption
|
|
4605
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4606
|
+
: "";
|
|
4607
|
+
|
|
4608
|
+
return (
|
|
4609
|
+
`<figure class="kami-chart">` +
|
|
4610
|
+
figHeader +
|
|
4611
|
+
`<svg class="kc-svg" viewBox="0 0 ${W} ${totalH}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(data.title || "Flowchart")}">` +
|
|
4612
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4613
|
+
els.join("") +
|
|
4614
|
+
`</svg>` +
|
|
4615
|
+
captionEl +
|
|
4616
|
+
`</figure>`
|
|
4617
|
+
);
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
/** Swimlane · 2–4 horizontal lanes × 4–10 steps. Each step assigned
|
|
4621
|
+
* to a lane via `laneIdx`; rendered left-to-right in step order.
|
|
4622
|
+
* Connectors elbow when consecutive steps change lanes. Replaces
|
|
4623
|
+
* inline mermaid `sequenceDiagram` AND `journey`. */
|
|
4624
|
+
function renderKamiSwimlane(data) {
|
|
4625
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
4626
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
4627
|
+
const focalLane = typeof data.focalLane === "string" ? data.focalLane.trim() : "";
|
|
4628
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
4629
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
4630
|
+
? String(data.figNum) : "";
|
|
4631
|
+
const lanesRaw = Array.isArray(data.lanes) ? data.lanes : [];
|
|
4632
|
+
const lanes = lanesRaw
|
|
4633
|
+
.map((l) => typeof l === "string" ? l.trim() : "")
|
|
4634
|
+
.filter(Boolean)
|
|
4635
|
+
.slice(0, 4);
|
|
4636
|
+
if (lanes.length < 2) {
|
|
4637
|
+
return `<div class="kami-chart-error" data-err="too-few-lanes"></div>`;
|
|
4638
|
+
}
|
|
4639
|
+
const stepsRaw = Array.isArray(data.steps) ? data.steps : [];
|
|
4640
|
+
const steps = [];
|
|
4641
|
+
for (const s of stepsRaw) {
|
|
4642
|
+
if (!s || typeof s !== "object") continue;
|
|
4643
|
+
const label = typeof s.label === "string" ? s.label.trim() : "";
|
|
4644
|
+
const laneIdx = Number.isInteger(s.laneIdx) ? s.laneIdx : -1;
|
|
4645
|
+
if (!label || laneIdx < 0 || laneIdx >= lanes.length) continue;
|
|
4646
|
+
const kicker = typeof s.kicker === "string" ? s.kicker.trim() : "";
|
|
4647
|
+
const score = Number.isInteger(s.score) ? Math.max(1, Math.min(5, s.score)) : 0;
|
|
4648
|
+
steps.push({ label, laneIdx, kicker, score });
|
|
4649
|
+
if (steps.length >= 10) break;
|
|
4650
|
+
}
|
|
4651
|
+
if (steps.length < 3) {
|
|
4652
|
+
return `<div class="kami-chart-error" data-err="too-few-steps"></div>`;
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
// Geometry · viewBox 960 × variable. Lane label column on left (64w).
|
|
4656
|
+
const VBW = 960;
|
|
4657
|
+
const LANE_COL_W = 72;
|
|
4658
|
+
const LANE_H = 110;
|
|
4659
|
+
const PAD_TOP = 40;
|
|
4660
|
+
const PAD_BOTTOM = 30;
|
|
4661
|
+
const NLanes = lanes.length;
|
|
4662
|
+
const totalH = PAD_TOP + NLanes * LANE_H + PAD_BOTTOM;
|
|
4663
|
+
const PLOT_X0 = LANE_COL_W + 16, PLOT_X1 = VBW - 24;
|
|
4664
|
+
const PLOT_W = PLOT_X1 - PLOT_X0;
|
|
4665
|
+
const N = steps.length;
|
|
4666
|
+
const stepGap = PLOT_W / N;
|
|
4667
|
+
const NODE_W = Math.min(140, Math.round(stepGap * 0.78));
|
|
4668
|
+
const NODE_H = 48;
|
|
4669
|
+
|
|
4670
|
+
// Lane backgrounds + labels
|
|
4671
|
+
const laneEls = [];
|
|
4672
|
+
for (let i = 0; i < NLanes; i++) {
|
|
4673
|
+
const y = PAD_TOP + i * LANE_H;
|
|
4674
|
+
const isFocalLane = focalLane && lanes[i] === focalLane;
|
|
4675
|
+
const bgCls = isFocalLane ? "kc-sw-lane-bg-focal" : (i % 2 === 0 ? "kc-sw-lane-bg" : "kc-sw-lane-bg-alt");
|
|
4676
|
+
laneEls.push(`<rect class="${bgCls}" x="${LANE_COL_W}" y="${y}" width="${VBW - LANE_COL_W}" height="${LANE_H}"/>`);
|
|
4677
|
+
if (i > 0) {
|
|
4678
|
+
const dCls = isFocalLane || (focalLane && lanes[i - 1] === focalLane)
|
|
4679
|
+
? "kc-sw-lane-divider-focal" : "kc-sw-lane-divider";
|
|
4680
|
+
laneEls.push(`<line class="${dCls}" x1="${LANE_COL_W}" y1="${y}" x2="${VBW}" y2="${y}"/>`);
|
|
4681
|
+
}
|
|
4682
|
+
const labelCls = isFocalLane ? "kc-sw-lane-label kc-sw-lane-label-focal" : "kc-sw-lane-label";
|
|
4683
|
+
const laneTxt = lanes[i].length > 14 ? lanes[i].slice(0, 13) + "…" : lanes[i];
|
|
4684
|
+
laneEls.push(`<text class="${labelCls}" x="${LANE_COL_W / 2}" y="${y + LANE_H / 2 + 4}" text-anchor="middle" transform="rotate(-90 ${LANE_COL_W / 2} ${y + LANE_H / 2})">${escape(laneTxt.toUpperCase())}</text>`);
|
|
4685
|
+
}
|
|
4686
|
+
// Label column frame
|
|
4687
|
+
laneEls.unshift(`<rect class="kc-sw-label-col" x="0" y="${PAD_TOP}" width="${LANE_COL_W}" height="${NLanes * LANE_H}"/>`);
|
|
4688
|
+
|
|
4689
|
+
// Step positions
|
|
4690
|
+
const stepGeo = steps.map((s, i) => {
|
|
4691
|
+
const cx = Math.round(PLOT_X0 + stepGap * i + stepGap / 2);
|
|
4692
|
+
const cy = PAD_TOP + s.laneIdx * LANE_H + LANE_H / 2;
|
|
4693
|
+
return { cx, cy, x: cx - NODE_W / 2, y: cy - NODE_H / 2, ...s };
|
|
4694
|
+
});
|
|
4695
|
+
|
|
4696
|
+
// Connectors (drawn first so nodes overlap them)
|
|
4697
|
+
const connectorEls = [];
|
|
4698
|
+
for (let i = 0; i < stepGeo.length - 1; i++) {
|
|
4699
|
+
const a = stepGeo[i];
|
|
4700
|
+
const b = stepGeo[i + 1];
|
|
4701
|
+
const isFocal = focal && (a.label === focal || b.label === focal);
|
|
4702
|
+
const arrowCls = isFocal ? "kc-sw-arrow-focal" : "kc-sw-arrow";
|
|
4703
|
+
const headCls = isFocal ? "kc-sw-arrowhead-focal" : "kc-sw-arrowhead";
|
|
4704
|
+
if (a.laneIdx === b.laneIdx) {
|
|
4705
|
+
// Horizontal · from right edge of a to left edge of b
|
|
4706
|
+
const x1 = a.x + NODE_W;
|
|
4707
|
+
const x2 = b.x;
|
|
4708
|
+
const y = a.cy;
|
|
4709
|
+
connectorEls.push(`<line class="${arrowCls}" x1="${x1}" y1="${y}" x2="${x2}" y2="${y}"/>`);
|
|
4710
|
+
connectorEls.push(`<path class="${headCls}" d="M ${x2 - 5} ${y - 4} L ${x2} ${y} L ${x2 - 5} ${y + 4}"/>`);
|
|
4711
|
+
} else {
|
|
4712
|
+
// Elbow · right edge of a → down/up to b's lane → left edge of b
|
|
4713
|
+
const x1 = a.x + NODE_W;
|
|
4714
|
+
const x2 = b.x;
|
|
4715
|
+
const midX = Math.round((x1 + x2) / 2);
|
|
4716
|
+
const path = `M ${x1} ${a.cy} L ${midX} ${a.cy} L ${midX} ${b.cy} L ${x2} ${b.cy}`;
|
|
4717
|
+
connectorEls.push(`<path class="${arrowCls}" d="${path}"/>`);
|
|
4718
|
+
connectorEls.push(`<path class="${headCls}" d="M ${x2 - 5} ${b.cy - 4} L ${x2} ${b.cy} L ${x2 - 5} ${b.cy + 4}"/>`);
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
// Step nodes (reuse kc-fl-* classes for consistency)
|
|
4723
|
+
const nodeEls = stepGeo.map((s) => {
|
|
4724
|
+
const isFocal = focal && s.label === focal;
|
|
4725
|
+
const cls = isFocal ? "kc-fl-node-focal" : "kc-fl-node";
|
|
4726
|
+
const kickerCls = isFocal ? "kc-fl-kicker kc-fl-kicker-focal" : "kc-fl-kicker";
|
|
4727
|
+
const kicker = s.kicker || `STEP ${String(stepGeo.indexOf(s) + 1).padStart(2, "0")}`;
|
|
4728
|
+
const labelTxt = s.label.length > 18 ? s.label.slice(0, 17) + "…" : s.label;
|
|
4729
|
+
// Optional journey score · 5 dots (filled up to score)
|
|
4730
|
+
const scoreEls = s.score > 0
|
|
4731
|
+
? [1, 2, 3, 4, 5].map((n) =>
|
|
4732
|
+
`<circle class="${n <= s.score ? "kc-sw-score-dot" : "kc-sw-score-dot-faint"}" cx="${s.cx - 16 + (n - 1) * 8}" cy="${s.y + NODE_H + 12}" r="2.4"/>`,
|
|
4733
|
+
).join("")
|
|
4734
|
+
: "";
|
|
4735
|
+
return (
|
|
4736
|
+
`<rect class="${cls}" x="${s.x}" y="${s.y}" width="${NODE_W}" height="${NODE_H}" rx="6"/>` +
|
|
4737
|
+
`<text class="${kickerCls}" x="${s.x + 16}" y="${s.y + 16}">${escape(kicker.toUpperCase())}</text>` +
|
|
4738
|
+
`<text class="kc-fl-text" x="${s.cx}" y="${s.cy + 8}" text-anchor="middle">${escape(labelTxt)}</text>` +
|
|
4739
|
+
scoreEls
|
|
4740
|
+
);
|
|
4741
|
+
}).join("");
|
|
4742
|
+
|
|
4743
|
+
const figHeader =
|
|
4744
|
+
`<div class="kc-figheader">` +
|
|
4745
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4746
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
4747
|
+
`</div>`;
|
|
4748
|
+
|
|
4749
|
+
const captionEl = caption
|
|
4750
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4751
|
+
: "";
|
|
4752
|
+
|
|
4753
|
+
return (
|
|
4754
|
+
`<figure class="kami-chart">` +
|
|
4755
|
+
figHeader +
|
|
4756
|
+
`<svg class="kc-svg" viewBox="0 0 ${VBW} ${totalH}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "Swimlane")}">` +
|
|
4757
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4758
|
+
laneEls.join("") +
|
|
4759
|
+
connectorEls.join("") +
|
|
4760
|
+
nodeEls +
|
|
4761
|
+
`</svg>` +
|
|
4762
|
+
captionEl +
|
|
4763
|
+
`</figure>`
|
|
4764
|
+
);
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
/** State machine · 2–6 states in a linear row · forward arrows
|
|
4768
|
+
* auto-drawn between consecutive states (labels from `forwardLabels`
|
|
4769
|
+
* array, indexed by source position) · optional dashed back-arcs
|
|
4770
|
+
* drawn above for cycles. Replaces inline mermaid `stateDiagram-v2`. */
|
|
4771
|
+
function renderKamiStateMachine(data) {
|
|
4772
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
4773
|
+
const focal = typeof data.focal === "string" ? data.focal.trim() : "";
|
|
4774
|
+
const caption = typeof data.caption === "string" ? data.caption.trim() : "";
|
|
4775
|
+
const figNum = typeof data.figNum === "string" || typeof data.figNum === "number"
|
|
4776
|
+
? String(data.figNum) : "";
|
|
4777
|
+
const statesRaw = Array.isArray(data.states) ? data.states : [];
|
|
4778
|
+
const states = [];
|
|
4779
|
+
for (const st of statesRaw) {
|
|
4780
|
+
if (!st || typeof st !== "object") continue;
|
|
4781
|
+
const label = typeof st.label === "string" ? st.label.trim() : "";
|
|
4782
|
+
if (!label) continue;
|
|
4783
|
+
const hint = typeof st.hint === "string" ? st.hint.trim() : "";
|
|
4784
|
+
const terminal = !!st.terminal;
|
|
4785
|
+
states.push({ label, hint, terminal });
|
|
4786
|
+
if (states.length >= 6) break;
|
|
4787
|
+
}
|
|
4788
|
+
if (states.length < 2) {
|
|
4789
|
+
return `<div class="kami-chart-error" data-err="too-few-states"></div>`;
|
|
4790
|
+
}
|
|
4791
|
+
const forwardLabels = Array.isArray(data.forwardLabels) ? data.forwardLabels : [];
|
|
4792
|
+
const backsRaw = Array.isArray(data.backTransitions) ? data.backTransitions : [];
|
|
4793
|
+
const backs = [];
|
|
4794
|
+
for (const b of backsRaw) {
|
|
4795
|
+
if (!b || typeof b !== "object") continue;
|
|
4796
|
+
const fromIdx = Number.isInteger(b.fromIdx) ? b.fromIdx : -1;
|
|
4797
|
+
const toIdx = Number.isInteger(b.toIdx) ? b.toIdx : -1;
|
|
4798
|
+
if (fromIdx < 0 || toIdx < 0 || fromIdx >= states.length || toIdx >= states.length) continue;
|
|
4799
|
+
if (fromIdx <= toIdx) continue; // back arcs must go right-to-left
|
|
4800
|
+
const label = typeof b.label === "string" ? b.label.trim() : "";
|
|
4801
|
+
backs.push({ fromIdx, toIdx, label });
|
|
4802
|
+
if (backs.length >= 4) break;
|
|
4803
|
+
}
|
|
4804
|
+
const showStart = !!data.showStart;
|
|
4805
|
+
const showEnd = !!data.showEnd;
|
|
4806
|
+
|
|
4807
|
+
// Geometry · viewBox 960 × 460, states arranged left-to-right.
|
|
4808
|
+
const VBW = 960, VBH = 380;
|
|
4809
|
+
const STATE_W = 140, STATE_H = 64;
|
|
4810
|
+
const ROW_Y = backs.length > 0 ? 184 : 140;
|
|
4811
|
+
const N = states.length;
|
|
4812
|
+
|
|
4813
|
+
// Compute total horizontal space (including optional start dot + end marker)
|
|
4814
|
+
const startSpace = showStart ? 60 : 0;
|
|
4815
|
+
const endSpace = showEnd ? 60 : 0;
|
|
4816
|
+
const innerSpace = VBW - 48 - startSpace - endSpace;
|
|
4817
|
+
const slotW = innerSpace / N;
|
|
4818
|
+
const stateX = (i) => 24 + startSpace + slotW * i + (slotW - STATE_W) / 2;
|
|
4819
|
+
|
|
4820
|
+
const stateEls = [];
|
|
4821
|
+
const arrowEls = [];
|
|
4822
|
+
|
|
4823
|
+
// States
|
|
4824
|
+
states.forEach((s, i) => {
|
|
4825
|
+
const x = stateX(i);
|
|
4826
|
+
const y = ROW_Y;
|
|
4827
|
+
const isFocal = focal && s.label === focal;
|
|
4828
|
+
const cls = s.terminal
|
|
4829
|
+
? "kc-sm-state-terminal"
|
|
4830
|
+
: (isFocal ? "kc-sm-state-focal" : "kc-sm-state");
|
|
4831
|
+
const kickerCls = isFocal ? "kc-sm-kicker kc-sm-kicker-focal" : "kc-sm-kicker";
|
|
4832
|
+
const kickerTxt = s.terminal ? "TERMINAL" : (isFocal ? "FOCAL" : "STATE");
|
|
4833
|
+
const labelTxt = s.label.length > 16 ? s.label.slice(0, 15) + "…" : s.label;
|
|
4834
|
+
const hintTxt = s.hint && s.hint.length > 14 ? s.hint.slice(0, 13) + "…" : s.hint;
|
|
4835
|
+
stateEls.push(
|
|
4836
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${STATE_W}" height="${STATE_H}" rx="6"/>` +
|
|
4837
|
+
`<text class="${kickerCls}" x="${x + 16}" y="${y + 16}">${escape(kickerTxt)}</text>` +
|
|
4838
|
+
`<text class="kc-sm-state-label" x="${x + STATE_W / 2}" y="${y + 36}" text-anchor="middle">${escape(labelTxt)}</text>` +
|
|
4839
|
+
(hintTxt ? `<text class="kc-sm-state-hint" x="${x + STATE_W / 2}" y="${y + 52}" text-anchor="middle">${escape(hintTxt)}</text>` : ""),
|
|
4840
|
+
);
|
|
4841
|
+
});
|
|
4842
|
+
|
|
4843
|
+
// Forward arrows (consecutive)
|
|
4844
|
+
for (let i = 0; i < N - 1; i++) {
|
|
4845
|
+
const x1 = stateX(i) + STATE_W;
|
|
4846
|
+
const x2 = stateX(i + 1);
|
|
4847
|
+
const y = ROW_Y + STATE_H / 2;
|
|
4848
|
+
const a = states[i], b = states[i + 1];
|
|
4849
|
+
const isFocal = focal && (a.label === focal || b.label === focal);
|
|
4850
|
+
const arrowCls = isFocal ? "kc-sm-forward-focal" : "kc-sm-forward";
|
|
4851
|
+
const headCls = isFocal ? "kc-sm-arrowhead-focal" : "kc-sm-arrowhead";
|
|
4852
|
+
arrowEls.push(`<line class="${arrowCls}" x1="${x1}" y1="${y}" x2="${x2}" y2="${y}"/>`);
|
|
4853
|
+
arrowEls.push(`<path class="${headCls}" d="M ${x2 - 6} ${y - 4} L ${x2} ${y} L ${x2 - 6} ${y + 4}"/>`);
|
|
4854
|
+
const lbl = forwardLabels[i] && typeof forwardLabels[i] === "string" ? forwardLabels[i].trim() : "";
|
|
4855
|
+
if (lbl) {
|
|
4856
|
+
const midX = (x1 + x2) / 2;
|
|
4857
|
+
const lblTxt = lbl.toUpperCase();
|
|
4858
|
+
const w = lblTxt.length * 5 + 12;
|
|
4859
|
+
const labelCls = isFocal ? "kc-sm-transition-label kc-primary" : "kc-sm-transition-label";
|
|
4860
|
+
arrowEls.push(
|
|
4861
|
+
`<rect class="kc-sm-transition-label-bg" x="${midX - w / 2}" y="${y - 6}" width="${w}" height="12" rx="2"/>` +
|
|
4862
|
+
`<text class="${labelCls}" x="${midX}" y="${y + 3}" text-anchor="middle">${escape(lblTxt)}</text>`,
|
|
4863
|
+
);
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
// Back-arcs (dashed)
|
|
4868
|
+
backs.forEach((bk) => {
|
|
4869
|
+
const fromX = stateX(bk.fromIdx) + STATE_W / 2;
|
|
4870
|
+
const toX = stateX(bk.toIdx) + STATE_W / 2;
|
|
4871
|
+
const startX = fromX - STATE_W / 4;
|
|
4872
|
+
const endX = toX + STATE_W / 4;
|
|
4873
|
+
const apexY = ROW_Y - 56;
|
|
4874
|
+
const path = `M ${startX} ${ROW_Y} Q ${(startX + endX) / 2} ${apexY} ${endX} ${ROW_Y}`;
|
|
4875
|
+
arrowEls.push(`<path class="kc-sm-back" d="${path}"/>`);
|
|
4876
|
+
arrowEls.push(`<path class="kc-sm-arrowhead" d="M ${endX + 6} ${ROW_Y - 5} L ${endX} ${ROW_Y} L ${endX + 6} ${ROW_Y + 5}"/>`);
|
|
4877
|
+
if (bk.label) {
|
|
4878
|
+
const midX = (startX + endX) / 2;
|
|
4879
|
+
const lblTxt = bk.label.toUpperCase();
|
|
4880
|
+
const w = lblTxt.length * 5 + 14;
|
|
4881
|
+
arrowEls.push(
|
|
4882
|
+
`<rect class="kc-sm-transition-label-bg" x="${midX - w / 2}" y="${apexY + 8}" width="${w}" height="12" rx="2"/>` +
|
|
4883
|
+
`<text class="kc-sm-transition-label" x="${midX}" y="${apexY + 17}" text-anchor="middle">${escape(lblTxt)}</text>`,
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
});
|
|
4887
|
+
|
|
4888
|
+
// Start dot
|
|
4889
|
+
if (showStart) {
|
|
4890
|
+
const cx = 24 + startSpace - 30;
|
|
4891
|
+
const cy = ROW_Y + STATE_H / 2;
|
|
4892
|
+
const firstX = stateX(0);
|
|
4893
|
+
arrowEls.push(`<circle class="kc-sm-marker-start" cx="${cx}" cy="${cy}" r="6"/>`);
|
|
4894
|
+
arrowEls.push(`<line class="kc-sm-forward" x1="${cx + 6}" y1="${cy}" x2="${firstX}" y2="${cy}"/>`);
|
|
4895
|
+
arrowEls.push(`<path class="kc-sm-arrowhead" d="M ${firstX - 6} ${cy - 4} L ${firstX} ${cy} L ${firstX - 6} ${cy + 4}"/>`);
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
// End marker
|
|
4899
|
+
if (showEnd) {
|
|
4900
|
+
const cx = VBW - 24 - endSpace + 30;
|
|
4901
|
+
const cy = ROW_Y + STATE_H / 2;
|
|
4902
|
+
const lastX = stateX(N - 1) + STATE_W;
|
|
4903
|
+
arrowEls.push(`<line class="kc-sm-forward" x1="${lastX}" y1="${cy}" x2="${cx - 10}" y2="${cy}"/>`);
|
|
4904
|
+
arrowEls.push(`<path class="kc-sm-arrowhead" d="M ${cx - 16} ${cy - 4} L ${cx - 10} ${cy} L ${cx - 16} ${cy + 4}"/>`);
|
|
4905
|
+
arrowEls.push(`<circle class="kc-sm-marker-end-outer" cx="${cx}" cy="${cy}" r="8"/>`);
|
|
4906
|
+
arrowEls.push(`<circle class="kc-sm-marker-end-inner" cx="${cx}" cy="${cy}" r="4"/>`);
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
const figHeader =
|
|
4910
|
+
`<div class="kc-figheader">` +
|
|
4911
|
+
`<span class="kc-fignum">Figure${figNum ? " " + escape(figNum) : ""}</span>` +
|
|
4912
|
+
(title ? `<span class="kc-figtitle">${escape(title)}</span>` : "") +
|
|
4913
|
+
`</div>`;
|
|
4914
|
+
|
|
4915
|
+
const captionEl = caption
|
|
4916
|
+
? `<p class="kc-caption">${inline(escape(caption))}</p>`
|
|
4917
|
+
: "";
|
|
4918
|
+
|
|
4919
|
+
return (
|
|
4920
|
+
`<figure class="kami-chart">` +
|
|
4921
|
+
figHeader +
|
|
4922
|
+
`<svg class="kc-svg" viewBox="0 0 ${VBW} ${VBH}" xmlns="http://www.w3.org/2000/svg" aria-label="${escape(title || "State machine")}">` +
|
|
4923
|
+
`<rect class="kc-canvas" width="100%" height="100%"/>` +
|
|
4924
|
+
arrowEls.join("") +
|
|
4925
|
+
stateEls.join("") +
|
|
4926
|
+
`</svg>` +
|
|
4927
|
+
captionEl +
|
|
4928
|
+
`</figure>`
|
|
4929
|
+
);
|
|
4930
|
+
}
|
|
4931
|
+
|
|
3228
4932
|
/** Parse a fenced ```path-comparison block (strict JSON) and emit
|
|
3229
4933
|
* the side-by-side binary comparison: 2 columns, each with a mono
|
|
3230
4934
|
* verdict tag, serif path name, and bullet characteristics. Stance
|
|
@@ -3520,17 +5224,15 @@
|
|
|
3520
5224
|
}
|
|
3521
5225
|
const body = lines.slice(start, end).join("\n");
|
|
3522
5226
|
const idx = placeholders.length;
|
|
3523
|
-
//
|
|
3524
|
-
//
|
|
3525
|
-
// ```
|
|
3526
|
-
// etc.
|
|
3527
|
-
//
|
|
3528
|
-
//
|
|
3529
|
-
//
|
|
3530
|
-
//
|
|
3531
|
-
|
|
3532
|
-
// sanitizeMermaid handles the rest as before.
|
|
3533
|
-
const MERMAID_TYPES = new Set([
|
|
5227
|
+
// Legacy mermaid blocks · all chart types migrated to the
|
|
5228
|
+
// `kami-chart` pipeline in v0.16. Old briefs in the DB still
|
|
5229
|
+
// carry ```mermaid (or bare ```quadrantChart / ```flowchart /
|
|
5230
|
+
// ```sequenceDiagram / etc.) — render those as a clear
|
|
5231
|
+
// "legacy" placeholder so the section stays readable instead
|
|
5232
|
+
// of leaking raw mermaid source through the codeblock pre.
|
|
5233
|
+
// Catalog matches the mermaid 10 diagram registry the writer
|
|
5234
|
+
// used to emit.
|
|
5235
|
+
const LEGACY_MERMAID_TYPES = new Set([
|
|
3534
5236
|
"mermaid",
|
|
3535
5237
|
"quadrantchart", "xychart", "xychart-beta",
|
|
3536
5238
|
"flowchart", "graph",
|
|
@@ -3546,24 +5248,22 @@
|
|
|
3546
5248
|
"gitgraph",
|
|
3547
5249
|
"requirementdiagram", "requirement",
|
|
3548
5250
|
"c4context", "c4container", "c4component", "c4dynamic",
|
|
3549
|
-
// newer / -beta types in mermaid 10+
|
|
3550
5251
|
"sankey-beta", "sankey",
|
|
3551
5252
|
"block-beta", "block",
|
|
3552
5253
|
"info",
|
|
3553
5254
|
"packet-beta", "packet",
|
|
3554
5255
|
"architecture-beta", "architecture",
|
|
3555
5256
|
]);
|
|
3556
|
-
if (
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
? body
|
|
3563
|
-
: `${langRaw}\n${body}`;
|
|
3564
|
-
placeholders.push(`<pre class="mermaid">${escape(sanitizeMermaid(mermaidBody))}</pre>`);
|
|
5257
|
+
if (LEGACY_MERMAID_TYPES.has(lang)) {
|
|
5258
|
+
placeholders.push(
|
|
5259
|
+
`<div class="kami-chart-error" data-err="legacy-mermaid">` +
|
|
5260
|
+
`// legacy mermaid block · re-generate this brief to render in the current pipeline` +
|
|
5261
|
+
`</div>`,
|
|
5262
|
+
);
|
|
3565
5263
|
} else if (lang === "metric-strip") {
|
|
3566
5264
|
placeholders.push(renderMetricStrip(body));
|
|
5265
|
+
} else if (lang === "kami-chart") {
|
|
5266
|
+
placeholders.push(renderKamiChart(body));
|
|
3567
5267
|
} else if (lang === "path-comparison") {
|
|
3568
5268
|
placeholders.push(renderPathComparison(body));
|
|
3569
5269
|
} else if (lang === "views-compared") {
|
|
@@ -4862,1370 +6562,6 @@
|
|
|
4862
6562
|
// to missing sections (a report may skip Frame Shift, etc.).
|
|
4863
6563
|
decorateReport(document.querySelector("[data-report-body]"));
|
|
4864
6564
|
|
|
4865
|
-
// Render any mermaid blocks. Wrapped in try/catch — a syntax error in
|
|
4866
|
-
// one diagram should not blank the whole report. The mermaid script is
|
|
4867
|
-
// deferred, so it may not be available the instant `load()` resolves;
|
|
4868
|
-
// poll up to 5s before giving up and leaving the raw fenced text in
|
|
4869
|
-
// place as a graceful fallback.
|
|
4870
|
-
if (document.querySelector(".mermaid")) {
|
|
4871
|
-
const mermaid = await new Promise((resolve) => {
|
|
4872
|
-
if (window.mermaid) return resolve(window.mermaid);
|
|
4873
|
-
let elapsed = 0;
|
|
4874
|
-
const t = setInterval(() => {
|
|
4875
|
-
if (window.mermaid) { clearInterval(t); resolve(window.mermaid); }
|
|
4876
|
-
else if ((elapsed += 50) > 5000) { clearInterval(t); resolve(null); }
|
|
4877
|
-
}, 50);
|
|
4878
|
-
});
|
|
4879
|
-
if (mermaid) {
|
|
4880
|
-
try {
|
|
4881
|
-
// Pick mermaid theme variables based on the active spine.
|
|
4882
|
-
// Each spine uses neutral quadrant fills (no colored tints —
|
|
4883
|
-
// all four quadrants share one fill for a clean Gartner / BCG
|
|
4884
|
-
// matrix look) and the spine's accent for plotted points.
|
|
4885
|
-
const spineKey = (brief.spine && SPINES.has(brief.spine)) ? brief.spine : "boardroom-dark";
|
|
4886
|
-
// Per-spine palette · `vars` carries every colour the
|
|
4887
|
-
// mermaid theme block touches. Extended with `accent` /
|
|
4888
|
-
// `accentSoft` (single accent for monochrome mindmap +
|
|
4889
|
-
// gantt bars + sequence notes), `accentText` (legible text
|
|
4890
|
-
// on accent fills), and `linkText` (faint text on link
|
|
4891
|
-
// backgrounds) so flowchart / mindmap / gantt / sequence /
|
|
4892
|
-
// state diagrams pick up spine-coherent colours instead of
|
|
4893
|
-
// mermaid's rainbow defaults.
|
|
4894
|
-
// Per-spine mermaid theme · every value here is taken from
|
|
4895
|
-
// the spine's :root tokens (boardroom-dark.css / a16z-thesis.css /
|
|
4896
|
-
// etc.) so charts feel like a native exhibit of the spine
|
|
4897
|
-
// rather than a foreign visualization with its own palette.
|
|
4898
|
-
// Audit cadence: when a spine adds / renames a token, mirror
|
|
4899
|
-
// it here in the same change. Drift between spine tokens and
|
|
4900
|
-
// these `vars` is what makes charts read as "off-brand".
|
|
4901
|
-
//
|
|
4902
|
-
// Mapping rules (consistent across spines):
|
|
4903
|
-
// background → spine main bg (so chart canvas blends with frame)
|
|
4904
|
-
// quadrantFill→ spine paper-soft / panel / bg-soft (slight contrast for inner zones)
|
|
4905
|
-
// pointFill → primary brand/accent (data dots = load-bearing)
|
|
4906
|
-
// titleFill → --ink / --text (max-contrast text)
|
|
4907
|
-
// axisText → --ink-faint / --text-faint (subdued axis labels)
|
|
4908
|
-
// border → --rule (NOT rule-strong) (matches body hairlines)
|
|
4909
|
-
// inner → --rule-soft (lightest separator)
|
|
4910
|
-
// accent → spine accent token (general fill)
|
|
4911
|
-
// accentSoft → spine pale variant (washed accent for backgrounds)
|
|
4912
|
-
// palette → 4-step monochrome ramp from accent (cycled across pie/journey/git slices)
|
|
4913
|
-
const themes = {
|
|
4914
|
-
"boardroom-dark": {
|
|
4915
|
-
// theme: "base" + darkMode flag, NOT "dark". Mermaid 10's
|
|
4916
|
-
// built-in "dark" preset injects a violet-tinged primary
|
|
4917
|
-
// colour that bleeds into quadrant1Fill / quadrantPointFill
|
|
4918
|
-
// and ALSO overrides our themeVariables in some code paths
|
|
4919
|
-
// (the "Platform Strategy chart on boardroom-dark renders
|
|
4920
|
-
// purple" bug). "base" uses minimal defaults so our
|
|
4921
|
-
// explicit `vars` below are the only colour source.
|
|
4922
|
-
base: "base",
|
|
4923
|
-
darkMode: true,
|
|
4924
|
-
// Frame bg: pre.mermaid uses --panel-2 (#1A1A18). Match it
|
|
4925
|
-
// for the canvas so the SVG doesn't sit "inside" a darker
|
|
4926
|
-
// tray. Quadrant inner boxes go to --bg (#0A0A0A) so the
|
|
4927
|
-
// four boxes read as cut-outs against the canvas.
|
|
4928
|
-
vars: {
|
|
4929
|
-
background: "#1A1A18", // --panel-2 (matches pre.mermaid frame)
|
|
4930
|
-
quadrantFill: "#0A0A0A", // --bg (cut-out zones inside chart)
|
|
4931
|
-
quadrantText: "#8E8B83", // --text-soft
|
|
4932
|
-
pointFill: "#C9A46B", // --em / --lime · load-bearing gold for data
|
|
4933
|
-
pointText: "#C8C5BE", // --text
|
|
4934
|
-
titleFill: "#C8C5BE", // --text
|
|
4935
|
-
axisText: "#5C5A4D", // --text-faint (was too bright before)
|
|
4936
|
-
border: "#3A3934", // --line-bright
|
|
4937
|
-
inner: "#21211E", // --panel-3
|
|
4938
|
-
accent: "#C9A46B", // --em (load-bearing gold)
|
|
4939
|
-
accentSoft: "#5C4422", // --lime-dim
|
|
4940
|
-
accentText: "#0A0A0A", // --bg for contrast on accent fills
|
|
4941
|
-
},
|
|
4942
|
-
// Gold for the lead, then beige-neutrals — multi-bar
|
|
4943
|
-
// charts get one bold lead + 3 supporting tones rather
|
|
4944
|
-
// than 4 competing golds.
|
|
4945
|
-
palette: ["#C9A46B", "#B6B0A2", "#8E8B83", "#5C5A52"],
|
|
4946
|
-
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
|
|
4947
|
-
},
|
|
4948
|
-
"a16z-thesis": {
|
|
4949
|
-
base: "default",
|
|
4950
|
-
vars: {
|
|
4951
|
-
background: "#F7F3E8", // --bg / --paper
|
|
4952
|
-
quadrantFill: "#F2EAD8", // soft panel tone (between paper and rule-soft)
|
|
4953
|
-
quadrantText: "#57503F", // --ink-mid
|
|
4954
|
-
pointFill: "#8B6A2E", // --gold (precise spine value, was #876C2A)
|
|
4955
|
-
pointText: "#14110C", // --ink
|
|
4956
|
-
titleFill: "#14110C", // --ink
|
|
4957
|
-
axisText: "#847B65", // --ink-faint
|
|
4958
|
-
border: "#DCD3BD", // --rule (not --rule-strong)
|
|
4959
|
-
inner: "#E5DECC", // --rule-soft
|
|
4960
|
-
accent: "#8B6A2E", // --gold
|
|
4961
|
-
accentSoft: "#C4A865", // --gold-pale
|
|
4962
|
-
accentText: "#FFFFFF",
|
|
4963
|
-
},
|
|
4964
|
-
palette: ["#8B6A2E", "#A8843A", "#C4A865", "#E2CFA0"],
|
|
4965
|
-
fontFamily: '"Inter", "Helvetica Neue", -apple-system, system-ui, sans-serif',
|
|
4966
|
-
},
|
|
4967
|
-
"anthropic-essay": {
|
|
4968
|
-
base: "default",
|
|
4969
|
-
vars: {
|
|
4970
|
-
background: "#F4F0E8", // --paper (was --surface, slight off)
|
|
4971
|
-
quadrantFill: "#EDE6D6", // --paper-soft
|
|
4972
|
-
quadrantText: "#6B6359", // --ink-mid
|
|
4973
|
-
pointFill: "#A85A41", // --clay-deep · stronger contrast for data
|
|
4974
|
-
pointText: "#1A1814", // --ink (was --ink-soft, low contrast)
|
|
4975
|
-
titleFill: "#1A1814", // --ink
|
|
4976
|
-
axisText: "#978C7E", // --ink-faint
|
|
4977
|
-
border: "#DDD5C8", // --rule
|
|
4978
|
-
inner: "#E8E1D2", // --rule-soft
|
|
4979
|
-
accent: "#CC785C", // --clay (spine's --accent resolves here, NOT clay-deep)
|
|
4980
|
-
accentSoft: "#F2E0D5", // --clay-pale
|
|
4981
|
-
accentText: "#FFFFFF",
|
|
4982
|
-
},
|
|
4983
|
-
palette: ["#CC785C", "#A85A41", "#B65A3A", "#F2E0D5"],
|
|
4984
|
-
fontFamily: '"Charter", "Source Serif Pro", "Iowan Old Style", Georgia, serif',
|
|
4985
|
-
},
|
|
4986
|
-
"gartner-note": {
|
|
4987
|
-
base: "default",
|
|
4988
|
-
vars: {
|
|
4989
|
-
background: "#FFFFFF", // --bg
|
|
4990
|
-
quadrantFill: "#F5F7FA", // --bg-soft (was --bg-emphasis #FAFBFC)
|
|
4991
|
-
quadrantText: "#455364", // --ink-soft
|
|
4992
|
-
pointFill: "#0A4DA1", // --brand
|
|
4993
|
-
pointText: "#1A2332", // --ink
|
|
4994
|
-
titleFill: "#1A2332", // --ink
|
|
4995
|
-
axisText: "#6B7785", // --ink-faint
|
|
4996
|
-
border: "#DDE2E8", // --rule (was --rule-strong, too heavy)
|
|
4997
|
-
inner: "#E8ECF1", // --rule-soft
|
|
4998
|
-
accent: "#0A4DA1", // --brand
|
|
4999
|
-
accentSoft: "#E8F1FB", // --brand-pale
|
|
5000
|
-
accentText: "#FFFFFF",
|
|
5001
|
-
},
|
|
5002
|
-
palette: ["#0A4DA1", "#1A65BD", "#3F73B8", "#7AA0CF"],
|
|
5003
|
-
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif',
|
|
5004
|
-
},
|
|
5005
|
-
"mckinsey-deck": {
|
|
5006
|
-
base: "default",
|
|
5007
|
-
vars: {
|
|
5008
|
-
background: "#FFFFFF", // --bg
|
|
5009
|
-
quadrantFill: "#F8FAFD", // soft panel (slightly tinted)
|
|
5010
|
-
quadrantText: "#4A5870", // --ink-soft
|
|
5011
|
-
pointFill: "#2251FF", // --blue
|
|
5012
|
-
pointText: "#051C2C", // --ink / --navy
|
|
5013
|
-
titleFill: "#051C2C", // --navy
|
|
5014
|
-
axisText: "#758296", // --ink-faint
|
|
5015
|
-
border: "#D5DCE4", // --rule (was --rule-strong)
|
|
5016
|
-
inner: "#E5EAF1", // --rule-soft
|
|
5017
|
-
accent: "#2251FF", // --blue
|
|
5018
|
-
accentSoft: "#E5EDFA", // --blue-pale
|
|
5019
|
-
accentText: "#FFFFFF",
|
|
5020
|
-
},
|
|
5021
|
-
palette: ["#2251FF", "#1A3FBD", "#6285FF", "#B8C2D0"],
|
|
5022
|
-
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif',
|
|
5023
|
-
},
|
|
5024
|
-
"openai-paper": {
|
|
5025
|
-
base: "default",
|
|
5026
|
-
vars: {
|
|
5027
|
-
background: "#FFFFFF", // --bg
|
|
5028
|
-
quadrantFill: "#FAFAFA", // --paper
|
|
5029
|
-
quadrantText: "#404040", // --ink-soft
|
|
5030
|
-
pointFill: "#10A37F", // --teal
|
|
5031
|
-
pointText: "#0D0D0D", // --ink
|
|
5032
|
-
titleFill: "#0D0D0D", // --ink
|
|
5033
|
-
axisText: "#6E6E80", // --ink-faint
|
|
5034
|
-
border: "#E5E5E5", // --rule
|
|
5035
|
-
inner: "#EFEFEF", // --rule-soft
|
|
5036
|
-
accent: "#10A37F", // --teal
|
|
5037
|
-
accentSoft: "#E6F6F0", // --teal-pale
|
|
5038
|
-
accentText: "#FFFFFF",
|
|
5039
|
-
},
|
|
5040
|
-
palette: ["#10A37F", "#0E8C6D", "#52B89A", "#8FCDB7"],
|
|
5041
|
-
fontFamily: '"Söhne", "Inter", -apple-system, system-ui, sans-serif',
|
|
5042
|
-
},
|
|
5043
|
-
};
|
|
5044
|
-
const t = themes[spineKey];
|
|
5045
|
-
|
|
5046
|
-
// Inject spine palette as CSS variables on :root so the
|
|
5047
|
-
// CSS rules in the global <style> block (mermaid spine
|
|
5048
|
-
// CSS) resolve to the right colours. This is the
|
|
5049
|
-
// PRIMARY mechanism — CSS applies on every paint, no JS
|
|
5050
|
-
// timing race possible. The JS takeover below stays as
|
|
5051
|
-
// belt-and-suspenders for chart elements not covered by
|
|
5052
|
-
// the CSS rules.
|
|
5053
|
-
try {
|
|
5054
|
-
const root = document.documentElement.style;
|
|
5055
|
-
root.setProperty('--mm-q-fill', t.vars.quadrantFill);
|
|
5056
|
-
root.setProperty('--mm-q-bg', t.vars.background);
|
|
5057
|
-
root.setProperty('--mm-q-point', t.vars.pointFill);
|
|
5058
|
-
root.setProperty('--mm-q-border', t.vars.border);
|
|
5059
|
-
root.setProperty('--mm-q-text', t.vars.titleFill);
|
|
5060
|
-
root.setProperty('--mm-q-axis-text', t.vars.axisText);
|
|
5061
|
-
root.setProperty('--mm-q-quad-text', t.vars.quadrantText);
|
|
5062
|
-
root.setProperty('--mm-text', t.vars.titleFill);
|
|
5063
|
-
root.setProperty('--mm-axis-text', t.vars.axisText);
|
|
5064
|
-
root.setProperty('--mm-pal-0', t.palette[0]);
|
|
5065
|
-
root.setProperty('--mm-pal-1', t.palette[1]);
|
|
5066
|
-
root.setProperty('--mm-pal-2', t.palette[2]);
|
|
5067
|
-
root.setProperty('--mm-pal-3', t.palette[3]);
|
|
5068
|
-
} catch (_) {}
|
|
5069
|
-
|
|
5070
|
-
// Build pie / journey / state / git slice-color overrides
|
|
5071
|
-
// by cycling through the spine's monochrome `palette`.
|
|
5072
|
-
// Without these, mermaid 11+ paints pie1..pie12 +
|
|
5073
|
-
// fillType0..7 + git0..7 with its built-in rainbow set
|
|
5074
|
-
// (violet / teal / orange) — that's the source of the
|
|
5075
|
-
// "white + purple" charts on the dark spine.
|
|
5076
|
-
const sliceColors = {};
|
|
5077
|
-
for (let i = 0; i < 12; i++) {
|
|
5078
|
-
const c = t.palette[i % t.palette.length];
|
|
5079
|
-
sliceColors[`pie${i + 1}`] = c;
|
|
5080
|
-
}
|
|
5081
|
-
for (let i = 0; i < 8; i++) {
|
|
5082
|
-
const c = t.palette[i % t.palette.length];
|
|
5083
|
-
sliceColors[`fillType${i}`] = c;
|
|
5084
|
-
sliceColors[`git${i}`] = c;
|
|
5085
|
-
sliceColors[`gitInv${i}`] = c;
|
|
5086
|
-
}
|
|
5087
|
-
// themeCSS shapes the rendered SVG beyond what themeVariables
|
|
5088
|
-
// expose. The chart title is hidden because the markdown
|
|
5089
|
-
// already renders an H3 caption directly above each chart —
|
|
5090
|
-
// showing it twice is the single ugliest mermaid default.
|
|
5091
|
-
// Hide every mermaid chart's built-in <text> title — the markdown
|
|
5092
|
-
// already prints an H3 caption right above each diagram, and
|
|
5093
|
-
// showing it twice is mermaid's single ugliest default. Apply
|
|
5094
|
-
// across quadrant / xychart / pie / timeline.
|
|
5095
|
-
const themeCSS = `
|
|
5096
|
-
/* ── SVG canvas · paint the theme bg explicitly. Without
|
|
5097
|
-
this, mermaid leaves the SVG transparent and the parent
|
|
5098
|
-
<pre>'s background bleeds through inconsistently across
|
|
5099
|
-
diagram types — most visibly on dark spines where the
|
|
5100
|
-
browser default white shows through pie/journey gaps. */
|
|
5101
|
-
svg { background: ${t.vars.background}; }
|
|
5102
|
-
.pieOuterCircle { stroke: ${t.vars.border} !important; }
|
|
5103
|
-
/* ── Quadrant chart ── */
|
|
5104
|
-
g.quadrant-chart text { font-family: ${t.fontFamily}; }
|
|
5105
|
-
g.quadrant-point > circle, .quadrant-point circle {
|
|
5106
|
-
stroke: ${t.vars.background};
|
|
5107
|
-
stroke-width: 2px;
|
|
5108
|
-
}
|
|
5109
|
-
text.quadrant-title,
|
|
5110
|
-
.pieTitleText,
|
|
5111
|
-
.xy-chart .title-text,
|
|
5112
|
-
.timeline .title-text,
|
|
5113
|
-
.timeline-title { display: none !important; }
|
|
5114
|
-
|
|
5115
|
-
/* ── xychart-beta · refined bars ──
|
|
5116
|
-
Bar WIDTH is slimmed by the post-render JS mutation
|
|
5117
|
-
(see "xychart bar slimming" block after mermaid.run) --
|
|
5118
|
-
CSS scaleX on SVG rects is unreliable across browsers,
|
|
5119
|
-
so we mutate the rect attributes directly. Here we
|
|
5120
|
-
only handle the COLOUR pass: solid accent fill, no
|
|
5121
|
-
stroke, with per-position cycling through the spine's
|
|
5122
|
-
4-shade monochrome palette so multi-bar charts stagger
|
|
5123
|
-
tones rather than reading as one flat block. */
|
|
5124
|
-
.xy-chart .bar,
|
|
5125
|
-
g.bar-plot .bar,
|
|
5126
|
-
g.bar-plot rect,
|
|
5127
|
-
g.plot > rect {
|
|
5128
|
-
fill: ${t.vars.accent} !important;
|
|
5129
|
-
stroke: none !important;
|
|
5130
|
-
}
|
|
5131
|
-
.xy-chart .bar:nth-child(2n),
|
|
5132
|
-
g.bar-plot .bar:nth-child(2n),
|
|
5133
|
-
g.bar-plot rect:nth-child(2n),
|
|
5134
|
-
g.plot > rect:nth-child(2n) { fill: ${t.palette[1]} !important; }
|
|
5135
|
-
.xy-chart .bar:nth-child(3n),
|
|
5136
|
-
g.bar-plot .bar:nth-child(3n),
|
|
5137
|
-
g.bar-plot rect:nth-child(3n),
|
|
5138
|
-
g.plot > rect:nth-child(3n) { fill: ${t.palette[2]} !important; }
|
|
5139
|
-
.xy-chart .bar:nth-child(4n),
|
|
5140
|
-
g.bar-plot .bar:nth-child(4n),
|
|
5141
|
-
g.bar-plot rect:nth-child(4n),
|
|
5142
|
-
g.plot > rect:nth-child(4n) { fill: ${t.palette[3]} !important; }
|
|
5143
|
-
/* Axis / grid refinement · 0.5px-feel hairlines, label
|
|
5144
|
-
typography in the mono kicker register so the chart's
|
|
5145
|
-
typography aligns with the rest of the doc. */
|
|
5146
|
-
.xy-chart .background { fill: transparent !important; }
|
|
5147
|
-
.xy-chart .x-axis line,
|
|
5148
|
-
.xy-chart .y-axis line,
|
|
5149
|
-
.xy-chart line.tick {
|
|
5150
|
-
stroke: ${t.vars.border} !important;
|
|
5151
|
-
stroke-width: 1px !important;
|
|
5152
|
-
}
|
|
5153
|
-
.xy-chart .x-axis path,
|
|
5154
|
-
.xy-chart .y-axis path,
|
|
5155
|
-
.xy-chart path.domain {
|
|
5156
|
-
stroke: ${t.vars.titleFill} !important;
|
|
5157
|
-
stroke-width: 1px !important;
|
|
5158
|
-
fill: none !important;
|
|
5159
|
-
}
|
|
5160
|
-
.xy-chart .grid line,
|
|
5161
|
-
.xy-chart .gridline,
|
|
5162
|
-
.xy-chart line.grid {
|
|
5163
|
-
stroke: ${t.vars.inner} !important;
|
|
5164
|
-
stroke-width: 1px !important;
|
|
5165
|
-
stroke-dasharray: 2 3 !important;
|
|
5166
|
-
}
|
|
5167
|
-
.xy-chart text,
|
|
5168
|
-
.xy-chart .x-axis text,
|
|
5169
|
-
.xy-chart .y-axis text {
|
|
5170
|
-
font-family: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace !important;
|
|
5171
|
-
font-size: 10px !important;
|
|
5172
|
-
fill: ${t.vars.axisText} !important;
|
|
5173
|
-
letter-spacing: 0.06em !important;
|
|
5174
|
-
}
|
|
5175
|
-
/* Data labels (showDataLabel) in the kicker register too. */
|
|
5176
|
-
.xy-chart .data-label,
|
|
5177
|
-
.xy-chart text.data-label {
|
|
5178
|
-
font-family: "SF Mono", "JetBrains Mono", Menlo, monospace !important;
|
|
5179
|
-
font-size: 10px !important;
|
|
5180
|
-
fill: ${t.vars.titleFill} !important;
|
|
5181
|
-
}
|
|
5182
|
-
|
|
5183
|
-
/* ── Pie ── */
|
|
5184
|
-
.pieCircle { stroke: ${t.vars.background}; stroke-width: 2px; }
|
|
5185
|
-
text.slice, .pieLegendText { font-family: ${t.fontFamily}; }
|
|
5186
|
-
|
|
5187
|
-
/* ── Timeline ── */
|
|
5188
|
-
.timeline text { font-family: ${t.fontFamily}; }
|
|
5189
|
-
|
|
5190
|
-
/* ── Flowchart · compact nodes + 1px hairline edges ──
|
|
5191
|
-
Default flowchart text is 16px, padding generous, edges
|
|
5192
|
-
heavy. Push to 12.5px / 1px / tighter so the diagram
|
|
5193
|
-
reads as a precise schematic, not a presentation slide.
|
|
5194
|
-
IMPORTANT: do NOT force a line-height larger than 1.2
|
|
5195
|
-
on .nodeLabel — mermaid's foreignObject is sized at
|
|
5196
|
-
measurement time using the inherited font-size at that
|
|
5197
|
-
moment. If we then forcibly stretch line-height, the
|
|
5198
|
-
actually-rendered text exceeds the foreignObject height
|
|
5199
|
-
and the bottom of multi-line node labels gets clipped.
|
|
5200
|
-
Also zero out the wrapper div / paragraph margins so
|
|
5201
|
-
the inline-block measurement doesn't include browser
|
|
5202
|
-
default <p> margins that mermaid won't account for. */
|
|
5203
|
-
.flowchart foreignObject > div,
|
|
5204
|
-
.label > foreignObject > div {
|
|
5205
|
-
line-height: 1.2 !important;
|
|
5206
|
-
margin: 0 !important;
|
|
5207
|
-
padding: 0 !important;
|
|
5208
|
-
}
|
|
5209
|
-
.flowchart .nodeLabel,
|
|
5210
|
-
.flowchart-label foreignObject div,
|
|
5211
|
-
.label foreignObject div,
|
|
5212
|
-
.label foreignObject p,
|
|
5213
|
-
.label foreignObject span,
|
|
5214
|
-
.nodeLabel,
|
|
5215
|
-
.nodeLabel p,
|
|
5216
|
-
.nodeLabel span {
|
|
5217
|
-
font-family: ${t.fontFamily} !important;
|
|
5218
|
-
font-size: 12.5px !important;
|
|
5219
|
-
line-height: 1.2 !important;
|
|
5220
|
-
margin: 0 !important;
|
|
5221
|
-
padding: 0 !important;
|
|
5222
|
-
color: ${t.vars.titleFill} !important;
|
|
5223
|
-
}
|
|
5224
|
-
.flowchart .edgeLabel,
|
|
5225
|
-
.flowchart .edgeLabel p,
|
|
5226
|
-
.edgeLabel foreignObject div,
|
|
5227
|
-
.edgeLabel p,
|
|
5228
|
-
.edgeLabel,
|
|
5229
|
-
.edgeLabel span {
|
|
5230
|
-
font-family: ${t.fontFamily} !important;
|
|
5231
|
-
font-size: 11px !important;
|
|
5232
|
-
line-height: 1.2 !important;
|
|
5233
|
-
margin: 0 !important;
|
|
5234
|
-
padding: 0 !important;
|
|
5235
|
-
color: ${t.vars.axisText} !important;
|
|
5236
|
-
background: ${t.vars.background} !important;
|
|
5237
|
-
}
|
|
5238
|
-
.flowchart .node rect,
|
|
5239
|
-
.flowchart .node circle,
|
|
5240
|
-
.flowchart .node ellipse,
|
|
5241
|
-
.flowchart .node polygon,
|
|
5242
|
-
.node rect,
|
|
5243
|
-
.node circle,
|
|
5244
|
-
.node polygon,
|
|
5245
|
-
.node ellipse {
|
|
5246
|
-
stroke-width: 1px !important;
|
|
5247
|
-
stroke: ${t.vars.border} !important;
|
|
5248
|
-
fill: ${t.vars.quadrantFill} !important;
|
|
5249
|
-
}
|
|
5250
|
-
.flowchart .edgePath path,
|
|
5251
|
-
.flowchart-link,
|
|
5252
|
-
.edgePath .path,
|
|
5253
|
-
.flowchart-link path {
|
|
5254
|
-
stroke: ${t.vars.border} !important;
|
|
5255
|
-
stroke-width: 1px !important;
|
|
5256
|
-
fill: none !important;
|
|
5257
|
-
}
|
|
5258
|
-
.flowchart .marker,
|
|
5259
|
-
.flowchart .arrowheadPath,
|
|
5260
|
-
#arrowhead, marker#arrowhead path {
|
|
5261
|
-
fill: ${t.vars.border} !important;
|
|
5262
|
-
stroke: ${t.vars.border} !important;
|
|
5263
|
-
}
|
|
5264
|
-
.cluster rect {
|
|
5265
|
-
stroke-width: 1px !important;
|
|
5266
|
-
stroke: ${t.vars.border} !important;
|
|
5267
|
-
fill: ${t.vars.background} !important;
|
|
5268
|
-
}
|
|
5269
|
-
.cluster text { font-size: 11px !important; font-family: ${t.fontFamily} !important; fill: ${t.vars.axisText} !important; }
|
|
5270
|
-
|
|
5271
|
-
/* ── Mindmap · monochrome accent (override rainbow) ── */
|
|
5272
|
-
.mindmap g.node text,
|
|
5273
|
-
.mindmap g.section text,
|
|
5274
|
-
.mindmap g.section foreignObject div,
|
|
5275
|
-
.mindmap-node text,
|
|
5276
|
-
.mindmap-node .nodeLabel {
|
|
5277
|
-
font-family: ${t.fontFamily} !important;
|
|
5278
|
-
font-size: 12px !important;
|
|
5279
|
-
fill: ${t.vars.titleFill} !important;
|
|
5280
|
-
color: ${t.vars.titleFill} !important;
|
|
5281
|
-
}
|
|
5282
|
-
.mindmap-node circle,
|
|
5283
|
-
.mindmap-node rect,
|
|
5284
|
-
.mindmap-node polygon {
|
|
5285
|
-
stroke-width: 1px !important;
|
|
5286
|
-
stroke: ${t.vars.accent} !important;
|
|
5287
|
-
}
|
|
5288
|
-
.mindmap-edge,
|
|
5289
|
-
.mindmap-edges path {
|
|
5290
|
-
stroke: ${t.vars.border} !important;
|
|
5291
|
-
stroke-width: 1px !important;
|
|
5292
|
-
fill: none !important;
|
|
5293
|
-
}
|
|
5294
|
-
|
|
5295
|
-
/* ── Sequence diagram · compact actor boxes + thin arrows ── */
|
|
5296
|
-
text.actor,
|
|
5297
|
-
text.actor > tspan,
|
|
5298
|
-
.actor-box-text {
|
|
5299
|
-
font-family: ${t.fontFamily} !important;
|
|
5300
|
-
font-size: 12px !important;
|
|
5301
|
-
fill: ${t.vars.titleFill} !important;
|
|
5302
|
-
}
|
|
5303
|
-
.actor {
|
|
5304
|
-
stroke-width: 1px !important;
|
|
5305
|
-
stroke: ${t.vars.border} !important;
|
|
5306
|
-
fill: ${t.vars.quadrantFill} !important;
|
|
5307
|
-
}
|
|
5308
|
-
.actor-line,
|
|
5309
|
-
line.actor-line {
|
|
5310
|
-
stroke: ${t.vars.border} !important;
|
|
5311
|
-
stroke-width: 1px !important;
|
|
5312
|
-
}
|
|
5313
|
-
.messageText {
|
|
5314
|
-
font-family: ${t.fontFamily} !important;
|
|
5315
|
-
font-size: 11px !important;
|
|
5316
|
-
fill: ${t.vars.titleFill} !important;
|
|
5317
|
-
}
|
|
5318
|
-
.messageLine0, .messageLine1 {
|
|
5319
|
-
stroke-width: 1px !important;
|
|
5320
|
-
stroke: ${t.vars.titleFill} !important;
|
|
5321
|
-
}
|
|
5322
|
-
.note,
|
|
5323
|
-
rect.note {
|
|
5324
|
-
stroke: ${t.vars.border} !important;
|
|
5325
|
-
fill: ${t.vars.accentSoft} !important;
|
|
5326
|
-
stroke-width: 1px !important;
|
|
5327
|
-
}
|
|
5328
|
-
.noteText,
|
|
5329
|
-
text.noteText,
|
|
5330
|
-
.note text {
|
|
5331
|
-
font-family: ${t.fontFamily} !important;
|
|
5332
|
-
font-size: 11px !important;
|
|
5333
|
-
fill: ${t.vars.titleFill} !important;
|
|
5334
|
-
}
|
|
5335
|
-
.activation0, .activation1, .activation2 {
|
|
5336
|
-
stroke: ${t.vars.border} !important;
|
|
5337
|
-
fill: ${t.vars.inner} !important;
|
|
5338
|
-
}
|
|
5339
|
-
|
|
5340
|
-
/* ── State diagram (v2) ── */
|
|
5341
|
-
.stateGroup rect,
|
|
5342
|
-
.stateGroup circle,
|
|
5343
|
-
.stateGroup line {
|
|
5344
|
-
stroke-width: 1px !important;
|
|
5345
|
-
}
|
|
5346
|
-
.stateLabel,
|
|
5347
|
-
.state-description,
|
|
5348
|
-
.stateGroup text {
|
|
5349
|
-
font-family: ${t.fontFamily} !important;
|
|
5350
|
-
font-size: 11.5px !important;
|
|
5351
|
-
fill: ${t.vars.titleFill} !important;
|
|
5352
|
-
}
|
|
5353
|
-
.transition {
|
|
5354
|
-
stroke: ${t.vars.border} !important;
|
|
5355
|
-
stroke-width: 1px !important;
|
|
5356
|
-
fill: none !important;
|
|
5357
|
-
}
|
|
5358
|
-
.compositeBackground,
|
|
5359
|
-
.composit {
|
|
5360
|
-
fill: ${t.vars.background} !important;
|
|
5361
|
-
stroke: ${t.vars.border} !important;
|
|
5362
|
-
}
|
|
5363
|
-
.stateGroup .innerCircle {
|
|
5364
|
-
fill: ${t.vars.titleFill} !important;
|
|
5365
|
-
}
|
|
5366
|
-
|
|
5367
|
-
/* ── Gantt · tight bars + small grid ── */
|
|
5368
|
-
.taskText,
|
|
5369
|
-
.taskTextOutsideRight,
|
|
5370
|
-
.taskTextOutsideLeft,
|
|
5371
|
-
text.taskText {
|
|
5372
|
-
font-family: ${t.fontFamily} !important;
|
|
5373
|
-
font-size: 11px !important;
|
|
5374
|
-
}
|
|
5375
|
-
.tick text,
|
|
5376
|
-
g.tick text {
|
|
5377
|
-
font-family: ${t.fontFamily} !important;
|
|
5378
|
-
font-size: 10px !important;
|
|
5379
|
-
fill: ${t.vars.axisText} !important;
|
|
5380
|
-
}
|
|
5381
|
-
.grid .tick line,
|
|
5382
|
-
g.grid line {
|
|
5383
|
-
stroke: ${t.vars.inner} !important;
|
|
5384
|
-
stroke-width: 0.5px !important;
|
|
5385
|
-
}
|
|
5386
|
-
.grid path { stroke-width: 0 !important; }
|
|
5387
|
-
.titleText { display: none !important; }
|
|
5388
|
-
.section0 text, .section1 text, .section2 text, .section3 text,
|
|
5389
|
-
.sectionTitle0, .sectionTitle1, .sectionTitle2, .sectionTitle3 {
|
|
5390
|
-
font-family: ${t.fontFamily} !important;
|
|
5391
|
-
font-size: 11px !important;
|
|
5392
|
-
fill: ${t.vars.axisText} !important;
|
|
5393
|
-
}
|
|
5394
|
-
rect.task,
|
|
5395
|
-
rect.task0, rect.task1, rect.task2, rect.task3,
|
|
5396
|
-
rect.activeTask0, rect.activeTask1, rect.activeTask2, rect.activeTask3 {
|
|
5397
|
-
stroke-width: 0 !important;
|
|
5398
|
-
}
|
|
5399
|
-
|
|
5400
|
-
/* ── Journey · compact ── */
|
|
5401
|
-
.journey-section text,
|
|
5402
|
-
.journey-section foreignObject div {
|
|
5403
|
-
font-family: ${t.fontFamily} !important;
|
|
5404
|
-
font-size: 11px !important;
|
|
5405
|
-
fill: ${t.vars.titleFill} !important;
|
|
5406
|
-
}
|
|
5407
|
-
.face-rect { stroke-width: 1px !important; }
|
|
5408
|
-
`;
|
|
5409
|
-
mermaid.initialize({
|
|
5410
|
-
startOnLoad: false,
|
|
5411
|
-
theme: t.base,
|
|
5412
|
-
fontFamily: t.fontFamily,
|
|
5413
|
-
themeCSS,
|
|
5414
|
-
quadrantChart: {
|
|
5415
|
-
// Refined-compact · was 640×480; the chart fits inside a
|
|
5416
|
-
// 740–880px article column, and 480px tall reads as a
|
|
5417
|
-
// presentation slide rather than an inline exhibit.
|
|
5418
|
-
chartWidth: 460,
|
|
5419
|
-
chartHeight: 320,
|
|
5420
|
-
// Hide mermaid's own title slot — the H3 above the chart
|
|
5421
|
-
// is the caption. Setting padding/size to 0 keeps the
|
|
5422
|
-
// layout from leaving an empty band at the top.
|
|
5423
|
-
titlePadding: 0,
|
|
5424
|
-
titleFontSize: 0,
|
|
5425
|
-
titleTextMargin: 0,
|
|
5426
|
-
quadrantPadding: 8,
|
|
5427
|
-
quadrantInternalBorderStrokeWidth: 0.5,
|
|
5428
|
-
quadrantExternalBorderStrokeWidth: 1,
|
|
5429
|
-
quadrantLabelFontSize: 11,
|
|
5430
|
-
quadrantTextTopPadding: 10,
|
|
5431
|
-
pointRadius: 5,
|
|
5432
|
-
pointLabelFontSize: 11,
|
|
5433
|
-
pointTextPadding: 6,
|
|
5434
|
-
xAxisLabelFontSize: 11,
|
|
5435
|
-
xAxisLabelPadding: 6,
|
|
5436
|
-
yAxisLabelFontSize: 11,
|
|
5437
|
-
yAxisLabelPadding: 6,
|
|
5438
|
-
xAxisPosition: "bottom",
|
|
5439
|
-
yAxisPosition: "left",
|
|
5440
|
-
},
|
|
5441
|
-
themeVariables: {
|
|
5442
|
-
// Global default · drives mermaid's INTERNAL label
|
|
5443
|
-
// measurement before SVG/foreignObject sizing. Setting
|
|
5444
|
-
// it here keeps the box dimensions in sync with the
|
|
5445
|
-
// CSS-rendered font-size, so multi-line flowchart node
|
|
5446
|
-
// labels don't get vertically clipped (which they did
|
|
5447
|
-
// when the CSS forced 12px after mermaid measured at
|
|
5448
|
-
// its 16px default).
|
|
5449
|
-
fontSize: "12.5px",
|
|
5450
|
-
// darkMode flag tells mermaid whether to invert
|
|
5451
|
-
// computed contrast pairs — required when we feed it
|
|
5452
|
-
// dark surface colors via the "base" theme. Without
|
|
5453
|
-
// this, mermaid computes text colors assuming a light
|
|
5454
|
-
// canvas and ends up with low-contrast labels.
|
|
5455
|
-
darkMode: t.darkMode === true,
|
|
5456
|
-
background: t.vars.background,
|
|
5457
|
-
// primaryColor MUST NOT be the quadrantFill (which is
|
|
5458
|
-
// very dark on dark spines). Mermaid uses primaryColor
|
|
5459
|
-
// as the seed for many derived colours (cScale shades,
|
|
5460
|
-
// pie defaults, accent variations) — when it's near-
|
|
5461
|
-
// black the derivations come out muddy. Use the spine's
|
|
5462
|
-
// accent so derivations land in the brand family.
|
|
5463
|
-
primaryColor: t.vars.accent,
|
|
5464
|
-
primaryTextColor: t.vars.titleFill,
|
|
5465
|
-
primaryBorderColor: t.vars.border,
|
|
5466
|
-
lineColor: t.vars.border,
|
|
5467
|
-
secondaryColor: t.vars.accentSoft,
|
|
5468
|
-
tertiaryColor: t.vars.quadrantFill,
|
|
5469
|
-
// All 4 quadrants share the same fill — clean matrix.
|
|
5470
|
-
quadrant1Fill: t.vars.quadrantFill,
|
|
5471
|
-
quadrant2Fill: t.vars.quadrantFill,
|
|
5472
|
-
quadrant3Fill: t.vars.quadrantFill,
|
|
5473
|
-
quadrant4Fill: t.vars.quadrantFill,
|
|
5474
|
-
quadrant1TextFill: t.vars.quadrantText,
|
|
5475
|
-
quadrant2TextFill: t.vars.quadrantText,
|
|
5476
|
-
quadrant3TextFill: t.vars.quadrantText,
|
|
5477
|
-
quadrant4TextFill: t.vars.quadrantText,
|
|
5478
|
-
quadrantPointFill: t.vars.pointFill,
|
|
5479
|
-
quadrantPointTextFill: t.vars.pointText,
|
|
5480
|
-
quadrantTitleFill: t.vars.titleFill,
|
|
5481
|
-
quadrantXAxisTextFill: t.vars.axisText,
|
|
5482
|
-
quadrantYAxisTextFill: t.vars.axisText,
|
|
5483
|
-
quadrantInternalBorderStrokeFill: t.vars.inner,
|
|
5484
|
-
quadrantExternalBorderStrokeFill: t.vars.border,
|
|
5485
|
-
// ── Flowchart / generic node-graph ──
|
|
5486
|
-
mainBkg: t.vars.quadrantFill,
|
|
5487
|
-
nodeBorder: t.vars.border,
|
|
5488
|
-
clusterBkg: t.vars.background,
|
|
5489
|
-
clusterBorder: t.vars.border,
|
|
5490
|
-
defaultLinkColor: t.vars.border,
|
|
5491
|
-
edgeLabelBackground: t.vars.background,
|
|
5492
|
-
titleColor: t.vars.titleFill,
|
|
5493
|
-
// ── Mindmap · single accent everywhere (override rainbow) ──
|
|
5494
|
-
cScale0: t.vars.accent,
|
|
5495
|
-
cScale1: t.vars.accent,
|
|
5496
|
-
cScale2: t.vars.accent,
|
|
5497
|
-
cScale3: t.vars.accent,
|
|
5498
|
-
cScale4: t.vars.accent,
|
|
5499
|
-
cScale5: t.vars.accent,
|
|
5500
|
-
cScale6: t.vars.accent,
|
|
5501
|
-
cScale7: t.vars.accent,
|
|
5502
|
-
cScale8: t.vars.accent,
|
|
5503
|
-
cScale9: t.vars.accent,
|
|
5504
|
-
cScale10: t.vars.accent,
|
|
5505
|
-
cScale11: t.vars.accent,
|
|
5506
|
-
// ── Sequence diagram ──
|
|
5507
|
-
actorBkg: t.vars.quadrantFill,
|
|
5508
|
-
actorBorder: t.vars.border,
|
|
5509
|
-
actorTextColor: t.vars.titleFill,
|
|
5510
|
-
actorLineColor: t.vars.border,
|
|
5511
|
-
signalColor: t.vars.titleFill,
|
|
5512
|
-
signalTextColor: t.vars.titleFill,
|
|
5513
|
-
labelBoxBkgColor: t.vars.quadrantFill,
|
|
5514
|
-
labelBoxBorderColor: t.vars.border,
|
|
5515
|
-
labelTextColor: t.vars.titleFill,
|
|
5516
|
-
loopTextColor: t.vars.titleFill,
|
|
5517
|
-
noteBkgColor: t.vars.accentSoft,
|
|
5518
|
-
noteTextColor: t.vars.titleFill,
|
|
5519
|
-
noteBorderColor: t.vars.border,
|
|
5520
|
-
activationBorderColor: t.vars.border,
|
|
5521
|
-
activationBkgColor: t.vars.inner,
|
|
5522
|
-
// ── Gantt · single-accent bars + neutral grid ──
|
|
5523
|
-
taskBkgColor: t.vars.accent,
|
|
5524
|
-
taskTextColor: t.vars.accentText,
|
|
5525
|
-
taskTextDarkColor: t.vars.titleFill,
|
|
5526
|
-
taskTextLightColor: t.vars.accentText,
|
|
5527
|
-
taskTextOutsideColor: t.vars.titleFill,
|
|
5528
|
-
activeTaskBkgColor: t.vars.accent,
|
|
5529
|
-
activeTaskBorderColor: t.vars.accent,
|
|
5530
|
-
doneTaskBkgColor: t.vars.inner,
|
|
5531
|
-
doneTaskBorderColor: t.vars.border,
|
|
5532
|
-
critBkgColor: t.vars.accent,
|
|
5533
|
-
critBorderColor: t.vars.accent,
|
|
5534
|
-
gridColor: t.vars.inner,
|
|
5535
|
-
sectionBkgColor: t.vars.background,
|
|
5536
|
-
altSectionBkgColor: t.vars.quadrantFill,
|
|
5537
|
-
sectionBkgColor2: t.vars.background,
|
|
5538
|
-
todayLineColor: t.vars.accent,
|
|
5539
|
-
// ── Pie / journey / state / git · monochrome palette
|
|
5540
|
-
// Cycles t.palette across mermaid's per-slice color
|
|
5541
|
-
// slots so these chart types stop falling back to
|
|
5542
|
-
// mermaid's built-in violet/teal rainbow defaults
|
|
5543
|
-
// (the white+purple leak on dark spines).
|
|
5544
|
-
...sliceColors,
|
|
5545
|
-
// ── xychart-beta · single-accent bars + matched chrome.
|
|
5546
|
-
// Mermaid generates xychart SVG using these tokens BEFORE
|
|
5547
|
-
// our themeCSS overrides apply, so they have to be set
|
|
5548
|
-
// here to get the right fill at render time (not just
|
|
5549
|
-
// post-paint via CSS). Palette joined as comma-separated
|
|
5550
|
-
// string per mermaid's plotColorPalette contract.
|
|
5551
|
-
xyChart: {
|
|
5552
|
-
backgroundColor: "transparent",
|
|
5553
|
-
titleColor: t.vars.titleFill,
|
|
5554
|
-
xAxisLabelColor: t.vars.axisText,
|
|
5555
|
-
xAxisTitleColor: t.vars.titleFill,
|
|
5556
|
-
xAxisTickColor: t.vars.border,
|
|
5557
|
-
xAxisLineColor: t.vars.titleFill,
|
|
5558
|
-
yAxisLabelColor: t.vars.axisText,
|
|
5559
|
-
yAxisTitleColor: t.vars.titleFill,
|
|
5560
|
-
yAxisTickColor: t.vars.border,
|
|
5561
|
-
yAxisLineColor: t.vars.titleFill,
|
|
5562
|
-
plotColorPalette: t.palette.join(","),
|
|
5563
|
-
},
|
|
5564
|
-
pieTitleTextSize: "0px",
|
|
5565
|
-
pieTitleTextColor: t.vars.titleFill,
|
|
5566
|
-
pieSectionTextColor: t.vars.titleFill,
|
|
5567
|
-
pieSectionTextSize: "11px",
|
|
5568
|
-
pieLegendTextColor: t.vars.titleFill,
|
|
5569
|
-
pieLegendTextSize: "11px",
|
|
5570
|
-
pieStrokeColor: t.vars.background,
|
|
5571
|
-
pieStrokeWidth: "1px",
|
|
5572
|
-
pieOuterStrokeColor: t.vars.border,
|
|
5573
|
-
pieOuterStrokeWidth: "1px",
|
|
5574
|
-
pieOpacity: "1",
|
|
5575
|
-
},
|
|
5576
|
-
flowchart: {
|
|
5577
|
-
useMaxWidth: true,
|
|
5578
|
-
htmlLabels: true,
|
|
5579
|
-
// Tighter spacing · default 50/50/15 reads as a slide,
|
|
5580
|
-
// these values render as a precise schematic. `padding`
|
|
5581
|
-
// is the cluster (subgraph) padding, NOT node padding —
|
|
5582
|
-
// mermaid sizes node boxes from text dimensions + an
|
|
5583
|
-
// internal margin, so this value alone won't squeeze
|
|
5584
|
-
// node labels. Keep at 12 for readable subgraph titles.
|
|
5585
|
-
nodeSpacing: 36,
|
|
5586
|
-
rankSpacing: 42,
|
|
5587
|
-
padding: 12,
|
|
5588
|
-
curve: "basis",
|
|
5589
|
-
},
|
|
5590
|
-
sequence: {
|
|
5591
|
-
useMaxWidth: true,
|
|
5592
|
-
diagramMarginX: 12,
|
|
5593
|
-
diagramMarginY: 6,
|
|
5594
|
-
actorMargin: 24,
|
|
5595
|
-
boxMargin: 5,
|
|
5596
|
-
boxTextMargin: 3,
|
|
5597
|
-
noteMargin: 6,
|
|
5598
|
-
messageMargin: 18,
|
|
5599
|
-
mirrorActors: false,
|
|
5600
|
-
bottomMarginAdj: 0,
|
|
5601
|
-
actorFontSize: 11,
|
|
5602
|
-
messageFontSize: 10,
|
|
5603
|
-
noteFontSize: 10,
|
|
5604
|
-
},
|
|
5605
|
-
gantt: {
|
|
5606
|
-
useMaxWidth: true,
|
|
5607
|
-
fontSize: 10,
|
|
5608
|
-
sectionFontSize: 10,
|
|
5609
|
-
numberSectionStyles: 3,
|
|
5610
|
-
leftPadding: 64,
|
|
5611
|
-
topPadding: 18,
|
|
5612
|
-
rightPadding: 18,
|
|
5613
|
-
barGap: 3,
|
|
5614
|
-
barHeight: 14,
|
|
5615
|
-
gridLineStartPadding: 28,
|
|
5616
|
-
},
|
|
5617
|
-
mindmap: {
|
|
5618
|
-
useMaxWidth: true,
|
|
5619
|
-
padding: 6,
|
|
5620
|
-
},
|
|
5621
|
-
stateDiagram: {
|
|
5622
|
-
useMaxWidth: true,
|
|
5623
|
-
fontSize: 12,
|
|
5624
|
-
titleTopMargin: 0,
|
|
5625
|
-
padding: 6,
|
|
5626
|
-
miniPadding: 4,
|
|
5627
|
-
},
|
|
5628
|
-
state: {
|
|
5629
|
-
useMaxWidth: true,
|
|
5630
|
-
fontSize: 12,
|
|
5631
|
-
},
|
|
5632
|
-
journey: {
|
|
5633
|
-
useMaxWidth: true,
|
|
5634
|
-
fontSize: 11,
|
|
5635
|
-
taskFontSize: 11,
|
|
5636
|
-
sectionFontSize: 12,
|
|
5637
|
-
},
|
|
5638
|
-
// ── xychart-beta · bar charts ──────────────────────────
|
|
5639
|
-
// Tight margins, label hidden (caption above), and a single
|
|
5640
|
-
// colour palette derived from the spine's pointFill so
|
|
5641
|
-
// every bar in a chart shares a coherent voice. Mermaid
|
|
5642
|
-
// 11+ accepts the `xyChart` key (older releases tolerated
|
|
5643
|
-
// `xychart` — keep BOTH so the config isn't fragile across
|
|
5644
|
-
// CDN bumps).
|
|
5645
|
-
xyChart: {
|
|
5646
|
-
// Was 720×360 · oversized for an inline exhibit. 560×260
|
|
5647
|
-
// sits comfortably inside a 740px article column without
|
|
5648
|
-
// pretending to be a presentation slide. Slightly shorter
|
|
5649
|
-
// than the 320px we tried first — squat-ish proportions
|
|
5650
|
-
// read as "data exhibit" while taller bars look like a
|
|
5651
|
-
// pitch slide.
|
|
5652
|
-
width: 560,
|
|
5653
|
-
height: 260,
|
|
5654
|
-
titlePadding: 0,
|
|
5655
|
-
titleFontSize: 0,
|
|
5656
|
-
showTitle: false,
|
|
5657
|
-
showDataLabel: true,
|
|
5658
|
-
// Refined bars · 50 → 32 → 20. mermaid xychart's bar
|
|
5659
|
-
// width is `slotWidth × reservedPercent / 100`, where
|
|
5660
|
-
// slotWidth scales inversely with bar count — so a value
|
|
5661
|
-
// tuned for 8 bars looks chunky on 3 bars (each slot
|
|
5662
|
-
// is ~3× wider). 20 keeps 8-12-bar charts legible while
|
|
5663
|
-
// 3-bar charts come out as ~36px-wide thin bars rather
|
|
5664
|
-
// than 60-90px filler blocks.
|
|
5665
|
-
plotReservedSpacePercent: 20,
|
|
5666
|
-
xAxis: { labelFontSize: 10, titleFontSize: 0, showTitle: false },
|
|
5667
|
-
yAxis: { labelFontSize: 10, titleFontSize: 10 },
|
|
5668
|
-
},
|
|
5669
|
-
// Pie · legend on the right, slice labels disabled so wide
|
|
5670
|
-
// labels don't push the chart off-screen. The legend
|
|
5671
|
-
// already carries label + value (showData: true above).
|
|
5672
|
-
pie: {
|
|
5673
|
-
textPosition: 0.7,
|
|
5674
|
-
useMaxWidth: true,
|
|
5675
|
-
},
|
|
5676
|
-
// Timeline · cards-on-rail; title suppressed (caption above).
|
|
5677
|
-
timeline: {
|
|
5678
|
-
padding: 16,
|
|
5679
|
-
useMaxWidth: true,
|
|
5680
|
-
},
|
|
5681
|
-
});
|
|
5682
|
-
// ── Off-screen render + paint-then-insert ────────────────
|
|
5683
|
-
// DEFINITIVE fix for "first time wrong, refresh OK" /
|
|
5684
|
-
// strict alternation pattern. Earlier strategies all let
|
|
5685
|
-
// mermaid render in-place, then patched colors after —
|
|
5686
|
-
// which gave the user a visible window of mermaid-default
|
|
5687
|
-
// colors before our takeover landed.
|
|
5688
|
-
//
|
|
5689
|
-
// The only race-free approach: render OFFSCREEN, paint our
|
|
5690
|
-
// spine palette onto the SVG while it's still detached,
|
|
5691
|
-
// THEN insert into the visible DOM. The user's first paint
|
|
5692
|
-
// sees the spine-colored SVG; there is no intermediate
|
|
5693
|
-
// mermaid-default state to perceive.
|
|
5694
|
-
//
|
|
5695
|
-
// mermaid 10's `render(id, src)` returns the SVG as a
|
|
5696
|
-
// string (not in-DOM) — perfect for this flow. We loop
|
|
5697
|
-
// through every `pre.mermaid`, render its source via
|
|
5698
|
-
// `mermaid.render`, parse the resulting string, apply the
|
|
5699
|
-
// takeover to the parsed (still-detached) SVG, then swap
|
|
5700
|
-
// the corrected SVG into the visible container.
|
|
5701
|
-
// Belt-and-suspenders fill setter · sets BOTH the inline
|
|
5702
|
-
// `style="fill: X !important"` AND the SVG `fill="X"`
|
|
5703
|
-
// presentation attribute. With mermaid's embedded <style>
|
|
5704
|
-
// already wiped, either layer is sufficient — but setting
|
|
5705
|
-
// both means even if one is somehow stripped/overridden
|
|
5706
|
-
// (browser quirk, late mermaid post-processing, CSS
|
|
5707
|
-
// cascade edge case), the chart still renders in the
|
|
5708
|
-
// spine palette.
|
|
5709
|
-
const setFill = (el, c) => {
|
|
5710
|
-
if (!c) return;
|
|
5711
|
-
try { el.style.setProperty('fill', c, 'important'); } catch (_) {}
|
|
5712
|
-
try { if (el.setAttribute) el.setAttribute('fill', c); } catch (_) {}
|
|
5713
|
-
};
|
|
5714
|
-
const setStroke = (el, c) => {
|
|
5715
|
-
if (c === undefined || c === null) return;
|
|
5716
|
-
try { el.style.setProperty('stroke', c, 'important'); } catch (_) {}
|
|
5717
|
-
try { if (el.setAttribute) el.setAttribute('stroke', c); } catch (_) {}
|
|
5718
|
-
};
|
|
5719
|
-
const setColor = (el, c) => { if (c) el.style.setProperty('color', c, 'important'); };
|
|
5720
|
-
const cyclePalette = (i) => t.palette[i % t.palette.length];
|
|
5721
|
-
|
|
5722
|
-
// Apply the spine palette to ONE SVG element (offscreen or
|
|
5723
|
-
// onscreen — works either way since every fill/stroke is
|
|
5724
|
-
// forced via inline `!important`). Pure function: no
|
|
5725
|
-
// global queries, only operates on the passed element.
|
|
5726
|
-
const applySpineToSvg = (svg) => {
|
|
5727
|
-
try {
|
|
5728
|
-
// ── 0. Pin the SVG max-width per chart type ──
|
|
5729
|
-
// Mermaid 10's chartWidth/chartHeight config is silently
|
|
5730
|
-
// ignored by the bundled CDN build, so the SVG ships at
|
|
5731
|
-
// its default size with `style="max-width: 500px"`
|
|
5732
|
-
// inline. CSS in <style> overrides it at paint time, but
|
|
5733
|
-
// there's a one-frame race on cold loads where the
|
|
5734
|
-
// user can perceive the unconstrained size before CSS
|
|
5735
|
-
// applies. Pin via inline style with !important here
|
|
5736
|
-
// for refined-compact dimensions on every first paint.
|
|
5737
|
-
const role = svg.getAttribute('aria-roledescription') || '';
|
|
5738
|
-
const chartMaxWidth = (
|
|
5739
|
-
role === 'quadrantChart' ? '460px' :
|
|
5740
|
-
role === 'xychart' ? '560px' :
|
|
5741
|
-
role === 'pie' ? '420px' :
|
|
5742
|
-
''
|
|
5743
|
-
);
|
|
5744
|
-
if (chartMaxWidth) {
|
|
5745
|
-
svg.style.setProperty('max-width', chartMaxWidth, 'important');
|
|
5746
|
-
svg.style.setProperty('height', 'auto', 'important');
|
|
5747
|
-
svg.style.setProperty('margin', '0 auto', 'important');
|
|
5748
|
-
svg.style.setProperty('display', 'block', 'important');
|
|
5749
|
-
}
|
|
5750
|
-
// ── 1. Wipe mermaid's embedded <style> block ──
|
|
5751
|
-
// Mermaid generates `<style>.quadrant-chart > .quadrant-
|
|
5752
|
-
// point > circle { fill: #...; }` etc. inside each SVG;
|
|
5753
|
-
// those CSS rules beat our setAttribute-set fills. After
|
|
5754
|
-
// removal, our inline-style fills below have no rivals.
|
|
5755
|
-
svg.querySelectorAll(':scope > style, defs > style').forEach((s) => s.remove());
|
|
5756
|
-
|
|
5757
|
-
// ── 2. SVG canvas / background ──
|
|
5758
|
-
// Any rect that fills the whole SVG is the chart canvas;
|
|
5759
|
-
// pin to t.vars.background so the chart blends with its
|
|
5760
|
-
// spine frame instead of showing mermaid's default white.
|
|
5761
|
-
const sw = parseFloat(svg.getAttribute('width') || '0');
|
|
5762
|
-
svg.querySelectorAll(':scope > rect, :scope > g > rect.background, rect.main-bkg').forEach((el) => {
|
|
5763
|
-
const w = parseFloat(el.getAttribute('width') || '0');
|
|
5764
|
-
if (w > 0 && sw > 0 && w >= sw * 0.95) setFill(el, t.vars.background);
|
|
5765
|
-
});
|
|
5766
|
-
|
|
5767
|
-
// ── 3. Quadrant chart ──
|
|
5768
|
-
// Mermaid 10's actual class names (verified against rendered DOM):
|
|
5769
|
-
// `<g class="quadrant">` — group; each contains a child <rect>
|
|
5770
|
-
// `.quadrants` — outer parent group
|
|
5771
|
-
// `<circle class="data-point">` — each data circle
|
|
5772
|
-
// `.data-points` — parent group
|
|
5773
|
-
// `.axis-line` / `.axisl-line` — axis lines
|
|
5774
|
-
// `.bottom-axis`, `.left-axis` — axis text containers
|
|
5775
|
-
// CRITICAL: the `.quadrant` class is on a <g>, but the
|
|
5776
|
-
// actual fillable rect is INSIDE the group. Selecting
|
|
5777
|
-
// `.quadrants > .quadrant` matches the <g> only;
|
|
5778
|
-
// setting style.fill on a <g> does NOT override the
|
|
5779
|
-
// child <rect>'s `fill="..."` attribute in SVG. We
|
|
5780
|
-
// must select the inner rect directly.
|
|
5781
|
-
//
|
|
5782
|
-
// Belt-and-suspenders: also match ALL <rect> inside
|
|
5783
|
-
// any quadrantChart SVG (excluding the canvas-bg rect
|
|
5784
|
-
// we already handled via dimensions). Catches any
|
|
5785
|
-
// mermaid version variation in class naming.
|
|
5786
|
-
svg.querySelectorAll('.quadrants .quadrant rect, g.quadrant > rect, rect.quadrant, rect.quadrant-fill, .quadrant-fill').forEach((el) => setFill(el, t.vars.quadrantFill));
|
|
5787
|
-
if (svg.getAttribute('aria-roledescription') === 'quadrantChart') {
|
|
5788
|
-
const qsw = parseFloat(svg.getAttribute('width') || '0');
|
|
5789
|
-
svg.querySelectorAll('rect').forEach((el) => {
|
|
5790
|
-
// Skip the canvas-bg rect (full-width).
|
|
5791
|
-
const w = parseFloat(el.getAttribute('width') || '0');
|
|
5792
|
-
if (qsw > 0 && w >= qsw * 0.95) return;
|
|
5793
|
-
setFill(el, t.vars.quadrantFill);
|
|
5794
|
-
});
|
|
5795
|
-
}
|
|
5796
|
-
// Quadrant border lines · mermaid 10 puts the outer
|
|
5797
|
-
// rectangle + cross-divider lines inside `<g class="border">`,
|
|
5798
|
-
// a sibling of `.quadrants` (NOT a descendant). Catch
|
|
5799
|
-
// both the inside-quadrants axis-line case AND the
|
|
5800
|
-
// border group case.
|
|
5801
|
-
svg.querySelectorAll('.axis-line, .axisl-line, .quadrants line, g.border line, .border line').forEach((el) => setStroke(el, t.vars.border));
|
|
5802
|
-
svg.querySelectorAll('.quadrant-internal-border, line.quadrant-internal-border').forEach((el) => setStroke(el, t.vars.inner));
|
|
5803
|
-
svg.querySelectorAll('.quadrant-external-border, rect.quadrant-external-border').forEach((el) => {
|
|
5804
|
-
setStroke(el, t.vars.border);
|
|
5805
|
-
setFill(el, 'none');
|
|
5806
|
-
});
|
|
5807
|
-
svg.querySelectorAll('text.quadrant-title, .quadrant-title, .chart-title').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5808
|
-
// Quadrant labels (the 4 corner labels). Mermaid 10
|
|
5809
|
-
// doesn't put them in a class; they're plain `<text>` inside
|
|
5810
|
-
// each `.quadrant` group. Walk children of `.quadrant` text.
|
|
5811
|
-
svg.querySelectorAll('.quadrant text, .quadrants text:not(.chart-title)').forEach((el) => setFill(el, t.vars.quadrantText));
|
|
5812
|
-
svg.querySelectorAll('text.quadrant-text, text.quadrant-quadrant-1-text-fill, text.quadrant-quadrant-2-text-fill, text.quadrant-quadrant-3-text-fill, text.quadrant-quadrant-4-text-fill').forEach((el) => setFill(el, t.vars.quadrantText));
|
|
5813
|
-
// Axis labels · "Low likelihood / High likelihood" etc.
|
|
5814
|
-
svg.querySelectorAll('.bottom-axis text, .left-axis text, .top-axis text, .right-axis text, text.x-axis-text-label, text.y-axis-text-label, .quadrant-x-axis-label, .quadrant-y-axis-label').forEach((el) => setFill(el, t.vars.axisText));
|
|
5815
|
-
// Data points · the load-bearing accent.
|
|
5816
|
-
svg.querySelectorAll('.data-point, .data-points circle, circle.data-point, .quadrant-point > circle, g.quadrant-point > circle, circle.quadrant-point').forEach((el) => {
|
|
5817
|
-
setFill(el, t.vars.pointFill);
|
|
5818
|
-
setStroke(el, t.vars.background);
|
|
5819
|
-
});
|
|
5820
|
-
svg.querySelectorAll('.data-point text, .data-points text, text.point-text, .quadrant-point text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5821
|
-
|
|
5822
|
-
// ── 4. Pie chart ──
|
|
5823
|
-
svg.querySelectorAll('path.pieCircle, g.pieCircle path, .slice').forEach((el, i) => {
|
|
5824
|
-
setFill(el, cyclePalette(i));
|
|
5825
|
-
setStroke(el, t.vars.background);
|
|
5826
|
-
});
|
|
5827
|
-
svg.querySelectorAll('.pieOuterCircle, circle.pieOuterCircle').forEach((el) => {
|
|
5828
|
-
setStroke(el, t.vars.border);
|
|
5829
|
-
setFill(el, 'none');
|
|
5830
|
-
});
|
|
5831
|
-
svg.querySelectorAll('text.slice, text.pieLegendText, .legend text, g.legend text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5832
|
-
svg.querySelectorAll('.pieTitleText').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5833
|
-
|
|
5834
|
-
// ── 5. xychart-beta ──
|
|
5835
|
-
// Bars cycle through palette; axes + grid + labels match
|
|
5836
|
-
// the spine's neutrals.
|
|
5837
|
-
svg.querySelectorAll('g.plot rect, g.bar-plot rect, g[class*="-plot"] rect, rect.bar').forEach((el, i) => {
|
|
5838
|
-
// Skip the plot-background rect (full plot width).
|
|
5839
|
-
const w = parseFloat(el.getAttribute('width') || '0');
|
|
5840
|
-
if (w >= 200) return;
|
|
5841
|
-
setFill(el, cyclePalette(i));
|
|
5842
|
-
setStroke(el, 'none');
|
|
5843
|
-
});
|
|
5844
|
-
svg.querySelectorAll('.xy-chart .background, rect.background').forEach((el) => setFill(el, 'transparent'));
|
|
5845
|
-
svg.querySelectorAll('.xy-chart text, g.xy-chart text, .xy-chart .x-axis text, .xy-chart .y-axis text').forEach((el) => setFill(el, t.vars.axisText));
|
|
5846
|
-
svg.querySelectorAll('.xy-chart .x-axis path.domain, .xy-chart .y-axis path.domain, .xy-chart .x-axis line, .xy-chart .y-axis line').forEach((el) => setStroke(el, t.vars.titleFill));
|
|
5847
|
-
svg.querySelectorAll('.xy-chart .grid line, .xy-chart line.grid').forEach((el) => setStroke(el, t.vars.inner));
|
|
5848
|
-
|
|
5849
|
-
// ── 6. Flowchart ──
|
|
5850
|
-
svg.querySelectorAll('.flowchart .node rect, .flowchart .node circle, .flowchart .node ellipse, .flowchart .node polygon, .node rect, .node circle, .node polygon, .node ellipse').forEach((el) => {
|
|
5851
|
-
setFill(el, t.vars.quadrantFill);
|
|
5852
|
-
setStroke(el, t.vars.border);
|
|
5853
|
-
});
|
|
5854
|
-
svg.querySelectorAll('.flowchart-link, .edgePath path, .edgePath .path, path.flowchart-link').forEach((el) => {
|
|
5855
|
-
setStroke(el, t.vars.border);
|
|
5856
|
-
setFill(el, 'none');
|
|
5857
|
-
});
|
|
5858
|
-
svg.querySelectorAll('marker path, #arrowhead path, marker#arrowhead path').forEach((el) => {
|
|
5859
|
-
setFill(el, t.vars.border);
|
|
5860
|
-
setStroke(el, t.vars.border);
|
|
5861
|
-
});
|
|
5862
|
-
svg.querySelectorAll('.cluster rect').forEach((el) => {
|
|
5863
|
-
setFill(el, t.vars.background);
|
|
5864
|
-
setStroke(el, t.vars.border);
|
|
5865
|
-
});
|
|
5866
|
-
svg.querySelectorAll('.cluster text, .nodeLabel, .label foreignObject *').forEach((el) => {
|
|
5867
|
-
setFill(el, t.vars.titleFill);
|
|
5868
|
-
setColor(el, t.vars.titleFill);
|
|
5869
|
-
});
|
|
5870
|
-
svg.querySelectorAll('.edgeLabel, .edgeLabel foreignObject *').forEach((el) => {
|
|
5871
|
-
setFill(el, t.vars.axisText);
|
|
5872
|
-
setColor(el, t.vars.axisText);
|
|
5873
|
-
el.style.setProperty('background-color', t.vars.background, 'important');
|
|
5874
|
-
});
|
|
5875
|
-
|
|
5876
|
-
// ── 7. Mindmap ──
|
|
5877
|
-
svg.querySelectorAll('.mindmap g.section circle, .mindmap-node circle, g.mindmap-node circle').forEach((el, i) => {
|
|
5878
|
-
setFill(el, cyclePalette(i));
|
|
5879
|
-
setStroke(el, t.vars.border);
|
|
5880
|
-
});
|
|
5881
|
-
svg.querySelectorAll('.mindmap g.section rect, .mindmap-node rect, g.mindmap-node rect').forEach((el, i) => {
|
|
5882
|
-
setFill(el, cyclePalette(i));
|
|
5883
|
-
setStroke(el, t.vars.border);
|
|
5884
|
-
});
|
|
5885
|
-
svg.querySelectorAll('.mindmap g.section polygon, .mindmap-node polygon, g.mindmap-node polygon').forEach((el, i) => {
|
|
5886
|
-
setFill(el, cyclePalette(i));
|
|
5887
|
-
setStroke(el, t.vars.border);
|
|
5888
|
-
});
|
|
5889
|
-
svg.querySelectorAll('.mindmap-edges path, .mindmap path.edge').forEach((el) => {
|
|
5890
|
-
setStroke(el, t.vars.border);
|
|
5891
|
-
setFill(el, 'none');
|
|
5892
|
-
});
|
|
5893
|
-
svg.querySelectorAll('.mindmap text, .mindmap-node text, g.mindmap-node text, .mindmap foreignObject *').forEach((el) => {
|
|
5894
|
-
setFill(el, t.vars.titleFill);
|
|
5895
|
-
setColor(el, t.vars.titleFill);
|
|
5896
|
-
});
|
|
5897
|
-
|
|
5898
|
-
// ── 8. Sequence diagram ──
|
|
5899
|
-
svg.querySelectorAll('rect.actor, .actor').forEach((el) => {
|
|
5900
|
-
setFill(el, t.vars.quadrantFill);
|
|
5901
|
-
setStroke(el, t.vars.border);
|
|
5902
|
-
});
|
|
5903
|
-
svg.querySelectorAll('line.actor-line, .actor-line').forEach((el) => setStroke(el, t.vars.border));
|
|
5904
|
-
svg.querySelectorAll('text.actor, text.actor tspan, .actor-box-text, .messageText, text.messageText, .noteText, text.noteText, .note text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5905
|
-
svg.querySelectorAll('.messageLine0, .messageLine1, line.messageLine0, line.messageLine1').forEach((el) => setStroke(el, t.vars.titleFill));
|
|
5906
|
-
svg.querySelectorAll('rect.note, .note').forEach((el) => {
|
|
5907
|
-
setFill(el, t.vars.accentSoft);
|
|
5908
|
-
setStroke(el, t.vars.border);
|
|
5909
|
-
});
|
|
5910
|
-
svg.querySelectorAll('.activation0, .activation1, .activation2, rect.activation0, rect.activation1, rect.activation2').forEach((el) => {
|
|
5911
|
-
setFill(el, t.vars.inner);
|
|
5912
|
-
setStroke(el, t.vars.border);
|
|
5913
|
-
});
|
|
5914
|
-
svg.querySelectorAll('.labelBox, rect.labelBox').forEach((el) => {
|
|
5915
|
-
setFill(el, t.vars.quadrantFill);
|
|
5916
|
-
setStroke(el, t.vars.border);
|
|
5917
|
-
});
|
|
5918
|
-
|
|
5919
|
-
// ── 9. State diagram ──
|
|
5920
|
-
svg.querySelectorAll('.stateGroup rect, g.stateGroup rect, rect.state').forEach((el) => {
|
|
5921
|
-
setFill(el, t.vars.quadrantFill);
|
|
5922
|
-
setStroke(el, t.vars.border);
|
|
5923
|
-
});
|
|
5924
|
-
svg.querySelectorAll('.stateGroup circle, g.stateGroup circle').forEach((el) => {
|
|
5925
|
-
setFill(el, t.vars.titleFill);
|
|
5926
|
-
setStroke(el, t.vars.titleFill);
|
|
5927
|
-
});
|
|
5928
|
-
svg.querySelectorAll('.stateGroup line, line.transition, path.transition').forEach((el) => {
|
|
5929
|
-
setStroke(el, t.vars.border);
|
|
5930
|
-
setFill(el, 'none');
|
|
5931
|
-
});
|
|
5932
|
-
svg.querySelectorAll('.stateLabel, .state-description, .stateGroup text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5933
|
-
svg.querySelectorAll('.compositeBackground, rect.compositeBackground').forEach((el) => {
|
|
5934
|
-
setFill(el, t.vars.background);
|
|
5935
|
-
setStroke(el, t.vars.border);
|
|
5936
|
-
});
|
|
5937
|
-
|
|
5938
|
-
// ── 10. Journey ──
|
|
5939
|
-
svg.querySelectorAll('.face-circle, circle.face-circle, .journey-section .face').forEach((el, i) => {
|
|
5940
|
-
setFill(el, cyclePalette(i));
|
|
5941
|
-
setStroke(el, t.vars.background);
|
|
5942
|
-
});
|
|
5943
|
-
svg.querySelectorAll('.journey-section text, .section text, .journey-section foreignObject *').forEach((el) => {
|
|
5944
|
-
setFill(el, t.vars.titleFill);
|
|
5945
|
-
setColor(el, t.vars.titleFill);
|
|
5946
|
-
});
|
|
5947
|
-
svg.querySelectorAll('rect.journey, .journey-section rect').forEach((el) => {
|
|
5948
|
-
setFill(el, t.vars.quadrantFill);
|
|
5949
|
-
setStroke(el, t.vars.border);
|
|
5950
|
-
});
|
|
5951
|
-
|
|
5952
|
-
// ── 11. Gantt ──
|
|
5953
|
-
svg.querySelectorAll('rect.task, rect.task0, rect.task1, rect.task2, rect.task3, rect.activeTask0, rect.activeTask1, rect.activeTask2, rect.activeTask3').forEach((el, i) => {
|
|
5954
|
-
setFill(el, cyclePalette(i));
|
|
5955
|
-
setStroke(el, 'none');
|
|
5956
|
-
});
|
|
5957
|
-
svg.querySelectorAll('rect.done, rect.done0, rect.done1, rect.done2, rect.done3, rect.doneTask').forEach((el) => {
|
|
5958
|
-
setFill(el, t.vars.inner);
|
|
5959
|
-
setStroke(el, t.vars.border);
|
|
5960
|
-
});
|
|
5961
|
-
svg.querySelectorAll('rect.crit, rect.crit0, rect.crit1, rect.crit2, rect.crit3').forEach((el) => {
|
|
5962
|
-
setFill(el, t.vars.accent);
|
|
5963
|
-
setStroke(el, t.vars.accent);
|
|
5964
|
-
});
|
|
5965
|
-
svg.querySelectorAll('.taskText, .taskTextOutsideRight, .taskTextOutsideLeft, text.taskText').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5966
|
-
svg.querySelectorAll('.tick text, g.tick text').forEach((el) => setFill(el, t.vars.axisText));
|
|
5967
|
-
svg.querySelectorAll('.tick line, .grid line, g.tick line, g.grid line').forEach((el) => setStroke(el, t.vars.inner));
|
|
5968
|
-
svg.querySelectorAll('.section0 text, .section1 text, .section2 text, .section3 text, .sectionTitle0, .sectionTitle1, .sectionTitle2, .sectionTitle3').forEach((el) => setFill(el, t.vars.axisText));
|
|
5969
|
-
svg.querySelectorAll('line.today, .today').forEach((el) => setStroke(el, t.vars.accent));
|
|
5970
|
-
|
|
5971
|
-
// ── 12. Timeline ──
|
|
5972
|
-
svg.querySelectorAll('.timeline rect, .timeline-section rect').forEach((el, i) => {
|
|
5973
|
-
setFill(el, cyclePalette(i));
|
|
5974
|
-
setStroke(el, t.vars.border);
|
|
5975
|
-
});
|
|
5976
|
-
svg.querySelectorAll('.timeline text, .timeline foreignObject *').forEach((el) => {
|
|
5977
|
-
setFill(el, t.vars.titleFill);
|
|
5978
|
-
setColor(el, t.vars.titleFill);
|
|
5979
|
-
});
|
|
5980
|
-
svg.querySelectorAll('.timeline path, .timeline-line').forEach((el) => {
|
|
5981
|
-
setStroke(el, t.vars.border);
|
|
5982
|
-
setFill(el, 'none');
|
|
5983
|
-
});
|
|
5984
|
-
|
|
5985
|
-
// ── 13. Git graph ──
|
|
5986
|
-
svg.querySelectorAll('.commit-bullets circle, circle.commit, .commit').forEach((el, i) => {
|
|
5987
|
-
setFill(el, cyclePalette(i));
|
|
5988
|
-
setStroke(el, t.vars.border);
|
|
5989
|
-
});
|
|
5990
|
-
svg.querySelectorAll('.commit-arrows path, path.commit-arrow, .commit-arrow').forEach((el) => {
|
|
5991
|
-
setStroke(el, t.vars.border);
|
|
5992
|
-
setFill(el, 'none');
|
|
5993
|
-
});
|
|
5994
|
-
svg.querySelectorAll('text.commit-label, .commit-label, .branch-label').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5995
|
-
|
|
5996
|
-
// ── 14. Generic title (every chart type) ──
|
|
5997
|
-
svg.querySelectorAll('text.titleText, .titleText, text.chart-title').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5998
|
-
|
|
5999
|
-
// ── 15. Universal text safety net ──
|
|
6000
|
-
// Any text element that survived the per-type passes
|
|
6001
|
-
// without an inline style fill — pin to titleFill so no
|
|
6002
|
-
// text leaks the mermaid-default purple/blue.
|
|
6003
|
-
svg.querySelectorAll('text').forEach((el) => {
|
|
6004
|
-
if (!el.style.fill) setFill(el, t.vars.titleFill);
|
|
6005
|
-
});
|
|
6006
|
-
|
|
6007
|
-
// ── 16. Mark the parent pre.mermaid as painted ──
|
|
6008
|
-
// CSS hides `.body pre.mermaid svg` until `.is-painted`
|
|
6009
|
-
// is added on the parent. We add it here so that even
|
|
6010
|
-
// when the SVG is later inserted into the live DOM, the
|
|
6011
|
-
// CSS-based hide is already lifted (no flash).
|
|
6012
|
-
const host = svg.closest('pre.mermaid');
|
|
6013
|
-
if (host && !host.classList.contains('is-painted')) {
|
|
6014
|
-
host.classList.add('is-painted');
|
|
6015
|
-
}
|
|
6016
|
-
} catch (forceColorErr) {
|
|
6017
|
-
console.warn('[mermaid] palette takeover failed for svg:', forceColorErr);
|
|
6018
|
-
}
|
|
6019
|
-
}; // applySpineToSvg
|
|
6020
|
-
|
|
6021
|
-
// Wrapper · walks every onscreen SVG and applies the
|
|
6022
|
-
// takeover. Used by the safety polling + MutationObserver
|
|
6023
|
-
// (defense-in-depth) for any SVGs that were already
|
|
6024
|
-
// inserted before the offscreen-render loop took over.
|
|
6025
|
-
const applySpinePaletteOnce = () => {
|
|
6026
|
-
document.querySelectorAll('.mermaid svg').forEach(applySpineToSvg);
|
|
6027
|
-
};
|
|
6028
|
-
|
|
6029
|
-
// ── Offscreen render via mermaid.run + DOM relocation ─
|
|
6030
|
-
// DEFINITIVE fix for "first load wrong / strict
|
|
6031
|
-
// alternation." Keep mermaid.run (it handles lazy module
|
|
6032
|
-
// loading for diagram types correctly — mermaid.render's
|
|
6033
|
-
// first call races with that loading on cold cache),
|
|
6034
|
-
// but move every pre.mermaid INTO AN OFFSCREEN CONTAINER
|
|
6035
|
-
// before running. mermaid renders inside the offscreen
|
|
6036
|
-
// container (user can't see it), we apply the spine
|
|
6037
|
-
// takeover to every resulting SVG, then move each
|
|
6038
|
-
// pre.mermaid back to its original DOM position. The
|
|
6039
|
-
// user's first paint of each chart shows the
|
|
6040
|
-
// spine-coloured SVG; no race possible.
|
|
6041
|
-
//
|
|
6042
|
-
// Diagnostic logs (console.info) so the user can verify
|
|
6043
|
-
// which path was taken and at what timing — open DevTools
|
|
6044
|
-
// and refresh; the log narrates the flow.
|
|
6045
|
-
const _mLog = (msg, ...rest) => {
|
|
6046
|
-
try { console.info(`[mermaid-spine] ${msg}`, ...rest); } catch (_) {}
|
|
6047
|
-
};
|
|
6048
|
-
_mLog('flow start · spine =', spineKey);
|
|
6049
|
-
const charts = Array.from(document.querySelectorAll('pre.mermaid'));
|
|
6050
|
-
_mLog('charts found:', charts.length);
|
|
6051
|
-
if (charts.length > 0) {
|
|
6052
|
-
// Capture original DOM positions so we can restore each
|
|
6053
|
-
// pre.mermaid exactly where it was.
|
|
6054
|
-
const anchors = charts.map((el) => ({
|
|
6055
|
-
el,
|
|
6056
|
-
parent: el.parentNode,
|
|
6057
|
-
nextSibling: el.nextSibling,
|
|
6058
|
-
}));
|
|
6059
|
-
// Build the offscreen host. Width matches a typical
|
|
6060
|
-
// article column so mermaid sizes the SVG correctly
|
|
6061
|
-
// (useMaxWidth: true uses the offscreen container's
|
|
6062
|
-
// dimensions for layout). Height: 0 + overflow: visible
|
|
6063
|
-
// so mermaid can compute proper SVG sizes.
|
|
6064
|
-
const offscreen = document.createElement('div');
|
|
6065
|
-
offscreen.id = 'mermaid-offscreen-host';
|
|
6066
|
-
offscreen.style.cssText = [
|
|
6067
|
-
'position: absolute',
|
|
6068
|
-
'left: -99999px',
|
|
6069
|
-
'top: 0',
|
|
6070
|
-
'width: 880px',
|
|
6071
|
-
'visibility: hidden',
|
|
6072
|
-
'pointer-events: none',
|
|
6073
|
-
'z-index: -1',
|
|
6074
|
-
].join('; ');
|
|
6075
|
-
document.body.appendChild(offscreen);
|
|
6076
|
-
// Move each pre.mermaid into the offscreen host.
|
|
6077
|
-
anchors.forEach((a) => offscreen.appendChild(a.el));
|
|
6078
|
-
_mLog('moved offscreen, calling mermaid.run');
|
|
6079
|
-
// Run mermaid · scoped to the offscreen host so it only
|
|
6080
|
-
// touches elements we just moved. mermaid handles lazy
|
|
6081
|
-
// module registration internally.
|
|
6082
|
-
try {
|
|
6083
|
-
await mermaid.run({ querySelector: '#mermaid-offscreen-host pre.mermaid' });
|
|
6084
|
-
_mLog('mermaid.run resolved');
|
|
6085
|
-
} catch (runErr) {
|
|
6086
|
-
console.warn('[mermaid-spine] offscreen run failed:', runErr);
|
|
6087
|
-
}
|
|
6088
|
-
// Wait for mermaid's deferred post-render work to land.
|
|
6089
|
-
// Quadrant chart (specifically) does d3-based label
|
|
6090
|
-
// positioning in a setTimeout(0) AFTER mermaid.run
|
|
6091
|
-
// resolves — if we apply takeover before that fires,
|
|
6092
|
-
// mermaid overwrites our fills. Two rAF + setTimeout(0)
|
|
6093
|
-
// cycles guarantee deferred work has settled. Cheap
|
|
6094
|
-
// (~32ms) and only runs once per page load.
|
|
6095
|
-
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(r, 0))));
|
|
6096
|
-
// Apply takeover to all SVGs while still offscreen.
|
|
6097
|
-
const svgsOffscreen = offscreen.querySelectorAll('svg');
|
|
6098
|
-
_mLog('SVGs rendered offscreen:', svgsOffscreen.length);
|
|
6099
|
-
svgsOffscreen.forEach(applySpineToSvg);
|
|
6100
|
-
_mLog('takeover applied offscreen');
|
|
6101
|
-
// Move each pre.mermaid back to its original position
|
|
6102
|
-
// and mark as painted (CSS reveal rule). Single DOM
|
|
6103
|
-
// mutation per chart → single paint frame with the
|
|
6104
|
-
// corrected SVG already in place.
|
|
6105
|
-
anchors.forEach((a) => {
|
|
6106
|
-
a.el.classList.add('is-painted');
|
|
6107
|
-
if (a.parent) {
|
|
6108
|
-
if (a.nextSibling && a.nextSibling.parentNode === a.parent) {
|
|
6109
|
-
a.parent.insertBefore(a.el, a.nextSibling);
|
|
6110
|
-
} else {
|
|
6111
|
-
a.parent.appendChild(a.el);
|
|
6112
|
-
}
|
|
6113
|
-
}
|
|
6114
|
-
// Re-apply takeover immediately after move · catches
|
|
6115
|
-
// any post-attach mermaid behaviour (ResizeObserver
|
|
6116
|
-
// re-renders, deferred rAF) that might restyle.
|
|
6117
|
-
a.el.querySelectorAll('svg').forEach(applySpineToSvg);
|
|
6118
|
-
});
|
|
6119
|
-
_mLog('moved back onscreen + reapplied takeover');
|
|
6120
|
-
// Cleanup offscreen host.
|
|
6121
|
-
offscreen.remove();
|
|
6122
|
-
}
|
|
6123
|
-
|
|
6124
|
-
// Safety net · if the takeover errors out for some reason
|
|
6125
|
-
// (script bug, exotic chart type), make sure every chart
|
|
6126
|
-
// becomes visible within 6s so the user at least sees the
|
|
6127
|
-
// chart (even if colours leak) instead of an empty frame.
|
|
6128
|
-
// Normal flow: each successful takeover pass marks its
|
|
6129
|
-
// pre.mermaid as `.is-painted` immediately, so the chart
|
|
6130
|
-
// appears as soon as the first pass succeeds (~16ms).
|
|
6131
|
-
setTimeout(() => {
|
|
6132
|
-
document.querySelectorAll('pre.mermaid:not(.is-painted)').forEach((el) => {
|
|
6133
|
-
el.classList.add('is-painted');
|
|
6134
|
-
});
|
|
6135
|
-
}, 6000);
|
|
6136
|
-
|
|
6137
|
-
// ── Continuous re-apply for the first 5 seconds ──
|
|
6138
|
-
// The user reported strict alternation ("1st wrong, 2nd
|
|
6139
|
-
// right, 3rd wrong, 4th right…") which means a single
|
|
6140
|
-
// setTimeout chain wasn't enough — mermaid's deferred
|
|
6141
|
-
// work was landing in different timing windows on
|
|
6142
|
-
// alternate loads, and my chain was missing odd-numbered
|
|
6143
|
-
// ones.
|
|
6144
|
-
//
|
|
6145
|
-
// Defense in depth: re-apply EVERY 60ms for the first 5
|
|
6146
|
-
// seconds. By the time the user perceives the chart, our
|
|
6147
|
-
// styles have been pinned 80+ times across every possible
|
|
6148
|
-
// mermaid post-render window. After 5s, switch to a pure
|
|
6149
|
-
// MutationObserver (only fires on real DOM changes) so we
|
|
6150
|
-
// keep up with future mutations without the polling cost.
|
|
6151
|
-
//
|
|
6152
|
-
// Each call is idempotent — same selectors set the same
|
|
6153
|
-
// inline-style values, so re-running is a cheap no-op
|
|
6154
|
-
// when nothing changed.
|
|
6155
|
-
applySpinePaletteOnce(); // sync, before next paint
|
|
6156
|
-
let pollCount = 0;
|
|
6157
|
-
const pollInterval = setInterval(() => {
|
|
6158
|
-
applySpinePaletteOnce();
|
|
6159
|
-
if (++pollCount > 80) clearInterval(pollInterval); // 80 × 60ms ≈ 4.8s
|
|
6160
|
-
}, 60);
|
|
6161
|
-
|
|
6162
|
-
// Persistent MutationObserver · catches any post-poll
|
|
6163
|
-
// mutations (container resize, font swap, late SSE
|
|
6164
|
-
// re-render). Watches structural changes only — our own
|
|
6165
|
-
// setProperty calls modify the `style` attribute, which
|
|
6166
|
-
// is excluded by `attributes: false` (default), so this
|
|
6167
|
-
// can't loop on itself.
|
|
6168
|
-
document.querySelectorAll('.mermaid svg').forEach((svg) => {
|
|
6169
|
-
const obs = new MutationObserver(() => {
|
|
6170
|
-
requestAnimationFrame(applySpinePaletteOnce);
|
|
6171
|
-
});
|
|
6172
|
-
obs.observe(svg, { childList: true, subtree: true });
|
|
6173
|
-
});
|
|
6174
|
-
// ── Post-render xychart bar slimming ─────────────────────
|
|
6175
|
-
// mermaid xychart-beta renders bars at near-full slot width
|
|
6176
|
-
// regardless of `plotReservedSpacePercent` in some 11.x
|
|
6177
|
-
// versions, and CSS transform: scaleX(...) on the bar rects
|
|
6178
|
-
// is unreliable across browsers (transform-box: fill-box
|
|
6179
|
-
// has Chrome/Safari gaps for SVG rect). Direct attribute
|
|
6180
|
-
// mutation is bulletproof: we walk every rendered xychart
|
|
6181
|
-
// SVG, identify the bar rects via class + geometric
|
|
6182
|
-
// heuristic (rect inside g.plot, height > width, not full-
|
|
6183
|
-
// plot wide), and shrink each rect's `width` to ~50% with
|
|
6184
|
-
// a matching `x` shift so it stays centered in its slot.
|
|
6185
|
-
// Result: a 3-bar chart goes from 3 chunky filler blocks
|
|
6186
|
-
// to 3 thin accents on a wide whitespace canvas — refined-
|
|
6187
|
-
// exhibit, not slide. */
|
|
6188
|
-
try {
|
|
6189
|
-
const xyHosts = document.querySelectorAll('.mermaid');
|
|
6190
|
-
xyHosts.forEach((host) => {
|
|
6191
|
-
const svg = host.querySelector('svg');
|
|
6192
|
-
if (!svg) return;
|
|
6193
|
-
// Only xychart-beta SVGs · skip quadrant / pie / etc.
|
|
6194
|
-
if (!svg.matches('[id^="xychart"], [aria-roledescription="xychart"]')
|
|
6195
|
-
&& !svg.querySelector('g.plot, g.bar-plot, g[class*="-plot"], rect.bar')) {
|
|
6196
|
-
return;
|
|
6197
|
-
}
|
|
6198
|
-
// Catch rects across mermaid 11.x naming conventions.
|
|
6199
|
-
const candidates = svg.querySelectorAll(
|
|
6200
|
-
'rect.bar, g.bar-plot rect, g.plot rect, g[class*="plot"] > rect'
|
|
6201
|
-
);
|
|
6202
|
-
candidates.forEach((rect) => {
|
|
6203
|
-
const x = parseFloat(rect.getAttribute('x'));
|
|
6204
|
-
const w = parseFloat(rect.getAttribute('width'));
|
|
6205
|
-
const h = parseFloat(rect.getAttribute('height'));
|
|
6206
|
-
if (!Number.isFinite(x) || !Number.isFinite(w) || !Number.isFinite(h)) return;
|
|
6207
|
-
// Skip the plot-background rect (full plot width).
|
|
6208
|
-
// Don't filter on h-vs-w ratio — that catches short
|
|
6209
|
-
// bars (small data values) and leaves them at full
|
|
6210
|
-
// width while their taller siblings shrink, giving
|
|
6211
|
-
// a mismatched chart.
|
|
6212
|
-
if (w >= 200) return;
|
|
6213
|
-
const scale = 0.5;
|
|
6214
|
-
const newW = w * scale;
|
|
6215
|
-
rect.setAttribute('width', newW.toFixed(2));
|
|
6216
|
-
rect.setAttribute('x', (x + (w - newW) / 2).toFixed(2));
|
|
6217
|
-
});
|
|
6218
|
-
});
|
|
6219
|
-
} catch (slimErr) {
|
|
6220
|
-
console.warn('[mermaid] xychart bar slimming failed:', slimErr);
|
|
6221
|
-
}
|
|
6222
|
-
} catch (e) {
|
|
6223
|
-
// Per-diagram failures already render as inline error overlays —
|
|
6224
|
-
// log and move on.
|
|
6225
|
-
console.warn("[mermaid] render failed:", e);
|
|
6226
|
-
}
|
|
6227
|
-
}
|
|
6228
|
-
}
|
|
6229
6565
|
}
|
|
6230
6566
|
|
|
6231
6567
|
/** Subscribe to room SSE so the title live-updates when the brief
|
|
@@ -6282,7 +6618,6 @@
|
|
|
6282
6618
|
});
|
|
6283
6619
|
})();
|
|
6284
6620
|
</script>
|
|
6285
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js" defer></script>
|
|
6286
6621
|
|
|
6287
6622
|
</body>
|
|
6288
6623
|
</html>
|