privateboard 0.1.6 → 0.1.8

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.
@@ -588,6 +588,168 @@
588
588
  border-top-width: 0;
589
589
  }
590
590
 
591
+ /* ─── Hide mermaid's internal render scratchpad ──────────────
592
+ During `mermaid.render(id, src)`, mermaid 10 briefly appends a
593
+ `<div id="dmermaid-...">` to document.body to measure layout +
594
+ produce the SVG. The div is removed once render() resolves,
595
+ but in the meantime it can flash visibly. Force it offscreen
596
+ so the user never sees mermaid's pre-takeover render at all.
597
+ The element still has natural dimensions for mermaid's layout
598
+ measurement — only its position + visibility is constrained. */
599
+ body > [id^="dmermaid-"] {
600
+ position: absolute !important;
601
+ left: -99999px !important;
602
+ top: 0 !important;
603
+ visibility: hidden !important;
604
+ pointer-events: none !important;
605
+ }
606
+
607
+ /* ─── Hide mermaid SVG until spine palette is applied ─────────
608
+ Pattern from user reports: "first time wrong, refresh OK" /
609
+ strict alternation (1st wrong, 2nd right, 3rd wrong, 4th
610
+ right). Cause: even with our continuous post-render takeover
611
+ (see report.html post-mermaid.run block), mermaid paints at
612
+ least once with its default palette BEFORE our takeover lands
613
+ — and the user sees that first paint. The takeover then
614
+ corrects it, but the user's already perceived "wrong colors."
615
+ On refresh, cached rendering may finish faster and the first
616
+ paint happens AFTER our takeover, which they perceive as
617
+ correct.
618
+ Defense: hide the SVG inside each pre.mermaid until we add
619
+ the `is-painted` class. The post-render takeover adds the
620
+ class as its last step. Until then, the user sees the frame
621
+ (matching the spine) with no SVG inside — no off-brand
622
+ colors flash at any point. */
623
+ .body pre.mermaid svg {
624
+ visibility: hidden;
625
+ }
626
+ .body pre.mermaid.is-painted svg {
627
+ visibility: visible;
628
+ }
629
+
630
+ /* ─── CSS-variable mermaid palette · timing-race-immune ──────
631
+ Final defense layer for the recurring "first time wrong,
632
+ refresh OK" bug. JS takeover fights timing with mermaid's
633
+ deferred render work; CSS doesn't have that problem — as
634
+ soon as the SVG is in the DOM, these rules apply. The
635
+ spine-specific colours are injected as CSS variables on
636
+ `:root` from JS in the swapSpine flow (see swapSpine /
637
+ mermaid.initialize block). Variables update when the spine
638
+ changes; CSS rules are static.
639
+
640
+ Specificity: each rule uses a 3-class+ chain plus
641
+ !important, beating mermaid's `.quadrant-fill { fill: ... }`
642
+ and any `fill="..."` presentation attribute. The element
643
+ selectors target every mermaid 10 chart-type's known
644
+ visual elements. */
645
+
646
+ /* Quadrant chart · 4 quadrant rects + data points */
647
+ .body pre.mermaid svg .quadrants .quadrant rect,
648
+ .body pre.mermaid svg g.quadrant > rect,
649
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] g.quadrant rect {
650
+ fill: var(--mm-q-fill, #1A1A18) !important;
651
+ }
652
+ .body pre.mermaid svg .data-points circle,
653
+ .body pre.mermaid svg circle.data-point,
654
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .data-points circle {
655
+ fill: var(--mm-q-point, #C9A46B) !important;
656
+ stroke: var(--mm-q-bg, #1A1A18) !important;
657
+ }
658
+ .body pre.mermaid svg .quadrants line,
659
+ .body pre.mermaid svg .axis-line,
660
+ .body pre.mermaid svg .axisl-line,
661
+ .body pre.mermaid svg g.border line,
662
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] line,
663
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] g.border line {
664
+ stroke: var(--mm-q-border, #3A3934) !important;
665
+ }
666
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] text {
667
+ fill: var(--mm-q-text, #C8C5BE) !important;
668
+ }
669
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .bottom-axis text,
670
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .left-axis text,
671
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .top-axis text,
672
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .right-axis text {
673
+ fill: var(--mm-q-axis-text, #5C5A4D) !important;
674
+ }
675
+ .body pre.mermaid svg [aria-roledescription="quadrantChart"] .quadrant text {
676
+ fill: var(--mm-q-quad-text, #8E8B83) !important;
677
+ }
678
+
679
+ /* xychart-beta · bars cycle through palette via :nth-child */
680
+ .body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect,
681
+ .body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect {
682
+ fill: var(--mm-pal-0, #C9A46B) !important;
683
+ stroke: none !important;
684
+ }
685
+ .body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(2),
686
+ .body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(2) {
687
+ fill: var(--mm-pal-1, #B6B0A2) !important;
688
+ }
689
+ .body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(3),
690
+ .body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(3) {
691
+ fill: var(--mm-pal-2, #8E8B83) !important;
692
+ }
693
+ .body pre.mermaid svg [aria-roledescription="xychart"] g[class*="bar-plot"] rect:nth-child(n+4),
694
+ .body pre.mermaid svg [aria-roledescription="xychart"] g.plot rect:nth-child(n+4) {
695
+ fill: var(--mm-pal-3, #5C5A52) !important;
696
+ }
697
+ .body pre.mermaid svg [aria-roledescription="xychart"] text {
698
+ fill: var(--mm-axis-text, #5C5A4D) !important;
699
+ }
700
+ .body pre.mermaid svg [aria-roledescription="xychart"] .background,
701
+ .body pre.mermaid svg [aria-roledescription="xychart"] rect.background {
702
+ fill: transparent !important;
703
+ }
704
+
705
+ /* Pie · slice palette */
706
+ .body pre.mermaid svg path.pieCircle:nth-of-type(1) { fill: var(--mm-pal-0, #C9A46B) !important; stroke: var(--mm-q-bg, #1A1A18) !important; }
707
+ .body pre.mermaid svg path.pieCircle:nth-of-type(2) { fill: var(--mm-pal-1, #B6B0A2) !important; }
708
+ .body pre.mermaid svg path.pieCircle:nth-of-type(3) { fill: var(--mm-pal-2, #8E8B83) !important; }
709
+ .body pre.mermaid svg path.pieCircle:nth-of-type(n+4) { fill: var(--mm-pal-3, #5C5A52) !important; }
710
+ .body pre.mermaid svg circle.pieOuterCircle {
711
+ stroke: var(--mm-q-border, #3A3934) !important;
712
+ fill: none !important;
713
+ }
714
+
715
+ /* Universal text safety net inside any mermaid SVG · titleFill
716
+ for anything that escaped the per-type rules above. Lower
717
+ specificity than the type-specific rules so they still win. */
718
+ .body pre.mermaid svg text {
719
+ fill: var(--mm-text, #C8C5BE); /* no !important · type-specific rules above WIN */
720
+ }
721
+
722
+ /* ─── Chart display sizes · CSS-enforced ─────────────────────
723
+ Mermaid 10.9.5's chartWidth/chartHeight config keys for
724
+ quadrantChart and xychart are silently ignored by the
725
+ bundled CDN build (verified via DOM dump showing default
726
+ 500×500 viewBox even when chartWidth: 460 is configured).
727
+ CSS-enforced sizes via `max-width` on the rendered SVG let
728
+ us get refined-compact dimensions regardless of what
729
+ mermaid's config validator does. The viewBox stays at
730
+ mermaid's internal coordinate space (500×500 etc.) but the
731
+ displayed SVG scales down to our target size. Centered with
732
+ margin: 0 auto so the chart sits flush in the article column
733
+ instead of stretching to full width. */
734
+ .body pre.mermaid svg[aria-roledescription="quadrantChart"] {
735
+ max-width: 460px !important;
736
+ height: auto !important;
737
+ margin: 0 auto !important;
738
+ display: block !important;
739
+ }
740
+ .body pre.mermaid svg[aria-roledescription="xychart"] {
741
+ max-width: 560px !important;
742
+ height: auto !important;
743
+ margin: 0 auto !important;
744
+ display: block !important;
745
+ }
746
+ .body pre.mermaid svg[aria-roledescription="pie"] {
747
+ max-width: 420px !important;
748
+ height: auto !important;
749
+ margin: 0 auto !important;
750
+ display: block !important;
751
+ }
752
+
591
753
  /* ─── Metric-strip · spine-agnostic baseline ─────────────────────
592
754
  Editorial / Swiss register: oversized thin numerals top-left, mute
593
755
  mono label bottom-left, hairline grid (no card backgrounds, no
@@ -4783,6 +4945,31 @@
4783
4945
  },
4784
4946
  };
4785
4947
  const t = themes[spineKey];
4948
+
4949
+ // Inject spine palette as CSS variables on :root so the
4950
+ // CSS rules in the global <style> block (mermaid spine
4951
+ // CSS) resolve to the right colours. This is the
4952
+ // PRIMARY mechanism — CSS applies on every paint, no JS
4953
+ // timing race possible. The JS takeover below stays as
4954
+ // belt-and-suspenders for chart elements not covered by
4955
+ // the CSS rules.
4956
+ try {
4957
+ const root = document.documentElement.style;
4958
+ root.setProperty('--mm-q-fill', t.vars.quadrantFill);
4959
+ root.setProperty('--mm-q-bg', t.vars.background);
4960
+ root.setProperty('--mm-q-point', t.vars.pointFill);
4961
+ root.setProperty('--mm-q-border', t.vars.border);
4962
+ root.setProperty('--mm-q-text', t.vars.titleFill);
4963
+ root.setProperty('--mm-q-axis-text', t.vars.axisText);
4964
+ root.setProperty('--mm-q-quad-text', t.vars.quadrantText);
4965
+ root.setProperty('--mm-text', t.vars.titleFill);
4966
+ root.setProperty('--mm-axis-text', t.vars.axisText);
4967
+ root.setProperty('--mm-pal-0', t.palette[0]);
4968
+ root.setProperty('--mm-pal-1', t.palette[1]);
4969
+ root.setProperty('--mm-pal-2', t.palette[2]);
4970
+ root.setProperty('--mm-pal-3', t.palette[3]);
4971
+ } catch (_) {}
4972
+
4786
4973
  // Build pie / journey / state / git slice-color overrides
4787
4974
  // by cycling through the spine's monochrome `palette`.
4788
4975
  // Without these, mermaid 11+ paints pie1..pie12 +
@@ -5395,74 +5582,498 @@
5395
5582
  useMaxWidth: true,
5396
5583
  },
5397
5584
  });
5398
- await mermaid.run({ querySelector: ".mermaid" });
5399
- // ── Post-render colour forcing ───────────────────────────
5400
- // Mermaid 10's themeVariables don't fully replace built-in
5401
- // colour defaults on every chart type quadrant fills,
5402
- // pie slices, journey faces and similar can leak the
5403
- // theme preset's primary colour (purple/violet on
5404
- // theme:"dark"). Walk every rendered SVG and force-set
5405
- // fills directly on the elements we know about. Bulletproof
5406
- // because it runs LAST, after mermaid's own paint.
5407
- try {
5408
- document.querySelectorAll('.mermaid svg').forEach((svg) => {
5409
- // ── Quadrant chart ──
5410
- // The four quadrant rects + internal/external borders +
5411
- // point dots. Mermaid 10 uses class `.quadrant-fill` on
5412
- // each filled rect inside the chart group.
5413
- svg.querySelectorAll('rect.quadrant-fill, .quadrant-fill').forEach((el) => {
5414
- el.setAttribute('fill', t.vars.quadrantFill);
5585
+ // ── Off-screen render + paint-then-insert ────────────────
5586
+ // DEFINITIVE fix for "first time wrong, refresh OK" /
5587
+ // strict alternation pattern. Earlier strategies all let
5588
+ // mermaid render in-place, then patched colors after
5589
+ // which gave the user a visible window of mermaid-default
5590
+ // colors before our takeover landed.
5591
+ //
5592
+ // The only race-free approach: render OFFSCREEN, paint our
5593
+ // spine palette onto the SVG while it's still detached,
5594
+ // THEN insert into the visible DOM. The user's first paint
5595
+ // sees the spine-colored SVG; there is no intermediate
5596
+ // mermaid-default state to perceive.
5597
+ //
5598
+ // mermaid 10's `render(id, src)` returns the SVG as a
5599
+ // string (not in-DOM) perfect for this flow. We loop
5600
+ // through every `pre.mermaid`, render its source via
5601
+ // `mermaid.render`, parse the resulting string, apply the
5602
+ // takeover to the parsed (still-detached) SVG, then swap
5603
+ // the corrected SVG into the visible container.
5604
+ // Belt-and-suspenders fill setter · sets BOTH the inline
5605
+ // `style="fill: X !important"` AND the SVG `fill="X"`
5606
+ // presentation attribute. With mermaid's embedded <style>
5607
+ // already wiped, either layer is sufficient — but setting
5608
+ // both means even if one is somehow stripped/overridden
5609
+ // (browser quirk, late mermaid post-processing, CSS
5610
+ // cascade edge case), the chart still renders in the
5611
+ // spine palette.
5612
+ const setFill = (el, c) => {
5613
+ if (!c) return;
5614
+ try { el.style.setProperty('fill', c, 'important'); } catch (_) {}
5615
+ try { if (el.setAttribute) el.setAttribute('fill', c); } catch (_) {}
5616
+ };
5617
+ const setStroke = (el, c) => {
5618
+ if (c === undefined || c === null) return;
5619
+ try { el.style.setProperty('stroke', c, 'important'); } catch (_) {}
5620
+ try { if (el.setAttribute) el.setAttribute('stroke', c); } catch (_) {}
5621
+ };
5622
+ const setColor = (el, c) => { if (c) el.style.setProperty('color', c, 'important'); };
5623
+ const cyclePalette = (i) => t.palette[i % t.palette.length];
5624
+
5625
+ // Apply the spine palette to ONE SVG element (offscreen or
5626
+ // onscreen — works either way since every fill/stroke is
5627
+ // forced via inline `!important`). Pure function: no
5628
+ // global queries, only operates on the passed element.
5629
+ const applySpineToSvg = (svg) => {
5630
+ try {
5631
+ // ── 0. Pin the SVG max-width per chart type ──
5632
+ // Mermaid 10's chartWidth/chartHeight config is silently
5633
+ // ignored by the bundled CDN build, so the SVG ships at
5634
+ // its default size with `style="max-width: 500px"`
5635
+ // inline. CSS in <style> overrides it at paint time, but
5636
+ // there's a one-frame race on cold loads where the
5637
+ // user can perceive the unconstrained size before CSS
5638
+ // applies. Pin via inline style with !important here
5639
+ // for refined-compact dimensions on every first paint.
5640
+ const role = svg.getAttribute('aria-roledescription') || '';
5641
+ const chartMaxWidth = (
5642
+ role === 'quadrantChart' ? '460px' :
5643
+ role === 'xychart' ? '560px' :
5644
+ role === 'pie' ? '420px' :
5645
+ ''
5646
+ );
5647
+ if (chartMaxWidth) {
5648
+ svg.style.setProperty('max-width', chartMaxWidth, 'important');
5649
+ svg.style.setProperty('height', 'auto', 'important');
5650
+ svg.style.setProperty('margin', '0 auto', 'important');
5651
+ svg.style.setProperty('display', 'block', 'important');
5652
+ }
5653
+ // ── 1. Wipe mermaid's embedded <style> block ──
5654
+ // Mermaid generates `<style>.quadrant-chart > .quadrant-
5655
+ // point > circle { fill: #...; }` etc. inside each SVG;
5656
+ // those CSS rules beat our setAttribute-set fills. After
5657
+ // removal, our inline-style fills below have no rivals.
5658
+ svg.querySelectorAll(':scope > style, defs > style').forEach((s) => s.remove());
5659
+
5660
+ // ── 2. SVG canvas / background ──
5661
+ // Any rect that fills the whole SVG is the chart canvas;
5662
+ // pin to t.vars.background so the chart blends with its
5663
+ // spine frame instead of showing mermaid's default white.
5664
+ const sw = parseFloat(svg.getAttribute('width') || '0');
5665
+ svg.querySelectorAll(':scope > rect, :scope > g > rect.background, rect.main-bkg').forEach((el) => {
5666
+ const w = parseFloat(el.getAttribute('width') || '0');
5667
+ if (w > 0 && sw > 0 && w >= sw * 0.95) setFill(el, t.vars.background);
5415
5668
  });
5416
- // Defense for any rect inside .quadrant-chart that wasn't
5417
- // class-tagged (mermaid version drift). Skip the chart's
5418
- // outer container rect (full-svg-width).
5419
- const chartG = svg.querySelector('g.quadrant-chart, .quadrant-chart');
5420
- if (chartG) {
5421
- const svgWidth = parseFloat(svg.getAttribute('width') || '0');
5422
- chartG.querySelectorAll(':scope > g > rect, :scope > rect').forEach((el) => {
5669
+
5670
+ // ── 3. Quadrant chart ──
5671
+ // Mermaid 10's actual class names (verified against rendered DOM):
5672
+ // `<g class="quadrant">` — group; each contains a child <rect>
5673
+ // `.quadrants` outer parent group
5674
+ // `<circle class="data-point">` each data circle
5675
+ // `.data-points` parent group
5676
+ // `.axis-line` / `.axisl-line` — axis lines
5677
+ // `.bottom-axis`, `.left-axis` — axis text containers
5678
+ // CRITICAL: the `.quadrant` class is on a <g>, but the
5679
+ // actual fillable rect is INSIDE the group. Selecting
5680
+ // `.quadrants > .quadrant` matches the <g> only;
5681
+ // setting style.fill on a <g> does NOT override the
5682
+ // child <rect>'s `fill="..."` attribute in SVG. We
5683
+ // must select the inner rect directly.
5684
+ //
5685
+ // Belt-and-suspenders: also match ALL <rect> inside
5686
+ // any quadrantChart SVG (excluding the canvas-bg rect
5687
+ // we already handled via dimensions). Catches any
5688
+ // mermaid version variation in class naming.
5689
+ svg.querySelectorAll('.quadrants .quadrant rect, g.quadrant > rect, rect.quadrant, rect.quadrant-fill, .quadrant-fill').forEach((el) => setFill(el, t.vars.quadrantFill));
5690
+ if (svg.getAttribute('aria-roledescription') === 'quadrantChart') {
5691
+ const qsw = parseFloat(svg.getAttribute('width') || '0');
5692
+ svg.querySelectorAll('rect').forEach((el) => {
5693
+ // Skip the canvas-bg rect (full-width).
5423
5694
  const w = parseFloat(el.getAttribute('width') || '0');
5424
- if (w > 0 && (svgWidth === 0 || w < svgWidth * 0.95)) {
5425
- el.setAttribute('fill', t.vars.quadrantFill);
5426
- }
5695
+ if (qsw > 0 && w >= qsw * 0.95) return;
5696
+ setFill(el, t.vars.quadrantFill);
5427
5697
  });
5428
5698
  }
5429
- // Quadrant data points · pointFill (the spine's
5430
- // strongest brand colour for marker-style data) with a
5431
- // frame-bg stroke for separation from the quadrant fill.
5432
- svg.querySelectorAll('circle.quadrant-point, .quadrant-point circle, g.quadrant-point > circle').forEach((el) => {
5433
- el.setAttribute('fill', t.vars.pointFill);
5434
- el.setAttribute('stroke', t.vars.background);
5699
+ // Quadrant border lines · mermaid 10 puts the outer
5700
+ // rectangle + cross-divider lines inside `<g class="border">`,
5701
+ // a sibling of `.quadrants` (NOT a descendant). Catch
5702
+ // both the inside-quadrants axis-line case AND the
5703
+ // border group case.
5704
+ svg.querySelectorAll('.axis-line, .axisl-line, .quadrants line, g.border line, .border line').forEach((el) => setStroke(el, t.vars.border));
5705
+ svg.querySelectorAll('.quadrant-internal-border, line.quadrant-internal-border').forEach((el) => setStroke(el, t.vars.inner));
5706
+ svg.querySelectorAll('.quadrant-external-border, rect.quadrant-external-border').forEach((el) => {
5707
+ setStroke(el, t.vars.border);
5708
+ setFill(el, 'none');
5435
5709
  });
5436
- // ── Pie chart slices · cycle palette, force stroke ──
5437
- svg.querySelectorAll('path.pieCircle').forEach((el, i) => {
5438
- el.setAttribute('fill', t.palette[i % t.palette.length]);
5439
- el.setAttribute('stroke', t.vars.background);
5710
+ svg.querySelectorAll('text.quadrant-title, .quadrant-title, .chart-title').forEach((el) => setFill(el, t.vars.titleFill));
5711
+ // Quadrant labels (the 4 corner labels). Mermaid 10
5712
+ // doesn't put them in a class; they're plain `<text>` inside
5713
+ // each `.quadrant` group. Walk children of `.quadrant` text.
5714
+ svg.querySelectorAll('.quadrant text, .quadrants text:not(.chart-title)').forEach((el) => setFill(el, t.vars.quadrantText));
5715
+ 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));
5716
+ // Axis labels · "Low likelihood / High likelihood" etc.
5717
+ 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));
5718
+ // Data points · the load-bearing accent.
5719
+ svg.querySelectorAll('.data-point, .data-points circle, circle.data-point, .quadrant-point > circle, g.quadrant-point > circle, circle.quadrant-point').forEach((el) => {
5720
+ setFill(el, t.vars.pointFill);
5721
+ setStroke(el, t.vars.background);
5722
+ });
5723
+ svg.querySelectorAll('.data-point text, .data-points text, text.point-text, .quadrant-point text').forEach((el) => setFill(el, t.vars.titleFill));
5724
+
5725
+ // ── 4. Pie chart ──
5726
+ svg.querySelectorAll('path.pieCircle, g.pieCircle path, .slice').forEach((el, i) => {
5727
+ setFill(el, cyclePalette(i));
5728
+ setStroke(el, t.vars.background);
5440
5729
  });
5441
- // Pie outer ring border.
5442
5730
  svg.querySelectorAll('.pieOuterCircle, circle.pieOuterCircle').forEach((el) => {
5443
- el.setAttribute('stroke', t.vars.border);
5444
- el.setAttribute('fill', 'none');
5731
+ setStroke(el, t.vars.border);
5732
+ setFill(el, 'none');
5733
+ });
5734
+ svg.querySelectorAll('text.slice, text.pieLegendText, .legend text, g.legend text').forEach((el) => setFill(el, t.vars.titleFill));
5735
+ svg.querySelectorAll('.pieTitleText').forEach((el) => setFill(el, t.vars.titleFill));
5736
+
5737
+ // ── 5. xychart-beta ──
5738
+ // Bars cycle through palette; axes + grid + labels match
5739
+ // the spine's neutrals.
5740
+ svg.querySelectorAll('g.plot rect, g.bar-plot rect, g[class*="-plot"] rect, rect.bar').forEach((el, i) => {
5741
+ // Skip the plot-background rect (full plot width).
5742
+ const w = parseFloat(el.getAttribute('width') || '0');
5743
+ if (w >= 200) return;
5744
+ setFill(el, cyclePalette(i));
5745
+ setStroke(el, 'none');
5746
+ });
5747
+ svg.querySelectorAll('.xy-chart .background, rect.background').forEach((el) => setFill(el, 'transparent'));
5748
+ 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));
5749
+ 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));
5750
+ svg.querySelectorAll('.xy-chart .grid line, .xy-chart line.grid').forEach((el) => setStroke(el, t.vars.inner));
5751
+
5752
+ // ── 6. Flowchart ──
5753
+ svg.querySelectorAll('.flowchart .node rect, .flowchart .node circle, .flowchart .node ellipse, .flowchart .node polygon, .node rect, .node circle, .node polygon, .node ellipse').forEach((el) => {
5754
+ setFill(el, t.vars.quadrantFill);
5755
+ setStroke(el, t.vars.border);
5756
+ });
5757
+ svg.querySelectorAll('.flowchart-link, .edgePath path, .edgePath .path, path.flowchart-link').forEach((el) => {
5758
+ setStroke(el, t.vars.border);
5759
+ setFill(el, 'none');
5760
+ });
5761
+ svg.querySelectorAll('marker path, #arrowhead path, marker#arrowhead path').forEach((el) => {
5762
+ setFill(el, t.vars.border);
5763
+ setStroke(el, t.vars.border);
5764
+ });
5765
+ svg.querySelectorAll('.cluster rect').forEach((el) => {
5766
+ setFill(el, t.vars.background);
5767
+ setStroke(el, t.vars.border);
5768
+ });
5769
+ svg.querySelectorAll('.cluster text, .nodeLabel, .label foreignObject *').forEach((el) => {
5770
+ setFill(el, t.vars.titleFill);
5771
+ setColor(el, t.vars.titleFill);
5772
+ });
5773
+ svg.querySelectorAll('.edgeLabel, .edgeLabel foreignObject *').forEach((el) => {
5774
+ setFill(el, t.vars.axisText);
5775
+ setColor(el, t.vars.axisText);
5776
+ el.style.setProperty('background-color', t.vars.background, 'important');
5777
+ });
5778
+
5779
+ // ── 7. Mindmap ──
5780
+ svg.querySelectorAll('.mindmap g.section circle, .mindmap-node circle, g.mindmap-node circle').forEach((el, i) => {
5781
+ setFill(el, cyclePalette(i));
5782
+ setStroke(el, t.vars.border);
5445
5783
  });
5446
- // ── Journey · face circles cycle palette ──
5447
- svg.querySelectorAll('circle.face-circle, .face-circle').forEach((el, i) => {
5448
- el.setAttribute('fill', t.palette[i % t.palette.length]);
5784
+ svg.querySelectorAll('.mindmap g.section rect, .mindmap-node rect, g.mindmap-node rect').forEach((el, i) => {
5785
+ setFill(el, cyclePalette(i));
5786
+ setStroke(el, t.vars.border);
5449
5787
  });
5450
- // ── Generic SVG canvas guard · any rect that fills the
5451
- // entire SVG and was painted with mermaid's default
5452
- // primaryColor (could be off-brand on dark spines). Cap
5453
- // its fill to t.vars.background.
5454
- const root = svg.querySelector(':scope > rect, :scope > g > rect.background');
5455
- if (root) {
5456
- const w = parseFloat(root.getAttribute('width') || '0');
5457
- const sw = parseFloat(svg.getAttribute('width') || '0');
5458
- if (w > 0 && sw > 0 && w >= sw * 0.95) {
5459
- root.setAttribute('fill', t.vars.background);
5788
+ svg.querySelectorAll('.mindmap g.section polygon, .mindmap-node polygon, g.mindmap-node polygon').forEach((el, i) => {
5789
+ setFill(el, cyclePalette(i));
5790
+ setStroke(el, t.vars.border);
5791
+ });
5792
+ svg.querySelectorAll('.mindmap-edges path, .mindmap path.edge').forEach((el) => {
5793
+ setStroke(el, t.vars.border);
5794
+ setFill(el, 'none');
5795
+ });
5796
+ svg.querySelectorAll('.mindmap text, .mindmap-node text, g.mindmap-node text, .mindmap foreignObject *').forEach((el) => {
5797
+ setFill(el, t.vars.titleFill);
5798
+ setColor(el, t.vars.titleFill);
5799
+ });
5800
+
5801
+ // ── 8. Sequence diagram ──
5802
+ svg.querySelectorAll('rect.actor, .actor').forEach((el) => {
5803
+ setFill(el, t.vars.quadrantFill);
5804
+ setStroke(el, t.vars.border);
5805
+ });
5806
+ svg.querySelectorAll('line.actor-line, .actor-line').forEach((el) => setStroke(el, t.vars.border));
5807
+ 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));
5808
+ svg.querySelectorAll('.messageLine0, .messageLine1, line.messageLine0, line.messageLine1').forEach((el) => setStroke(el, t.vars.titleFill));
5809
+ svg.querySelectorAll('rect.note, .note').forEach((el) => {
5810
+ setFill(el, t.vars.accentSoft);
5811
+ setStroke(el, t.vars.border);
5812
+ });
5813
+ svg.querySelectorAll('.activation0, .activation1, .activation2, rect.activation0, rect.activation1, rect.activation2').forEach((el) => {
5814
+ setFill(el, t.vars.inner);
5815
+ setStroke(el, t.vars.border);
5816
+ });
5817
+ svg.querySelectorAll('.labelBox, rect.labelBox').forEach((el) => {
5818
+ setFill(el, t.vars.quadrantFill);
5819
+ setStroke(el, t.vars.border);
5820
+ });
5821
+
5822
+ // ── 9. State diagram ──
5823
+ svg.querySelectorAll('.stateGroup rect, g.stateGroup rect, rect.state').forEach((el) => {
5824
+ setFill(el, t.vars.quadrantFill);
5825
+ setStroke(el, t.vars.border);
5826
+ });
5827
+ svg.querySelectorAll('.stateGroup circle, g.stateGroup circle').forEach((el) => {
5828
+ setFill(el, t.vars.titleFill);
5829
+ setStroke(el, t.vars.titleFill);
5830
+ });
5831
+ svg.querySelectorAll('.stateGroup line, line.transition, path.transition').forEach((el) => {
5832
+ setStroke(el, t.vars.border);
5833
+ setFill(el, 'none');
5834
+ });
5835
+ svg.querySelectorAll('.stateLabel, .state-description, .stateGroup text').forEach((el) => setFill(el, t.vars.titleFill));
5836
+ svg.querySelectorAll('.compositeBackground, rect.compositeBackground').forEach((el) => {
5837
+ setFill(el, t.vars.background);
5838
+ setStroke(el, t.vars.border);
5839
+ });
5840
+
5841
+ // ── 10. Journey ──
5842
+ svg.querySelectorAll('.face-circle, circle.face-circle, .journey-section .face').forEach((el, i) => {
5843
+ setFill(el, cyclePalette(i));
5844
+ setStroke(el, t.vars.background);
5845
+ });
5846
+ svg.querySelectorAll('.journey-section text, .section text, .journey-section foreignObject *').forEach((el) => {
5847
+ setFill(el, t.vars.titleFill);
5848
+ setColor(el, t.vars.titleFill);
5849
+ });
5850
+ svg.querySelectorAll('rect.journey, .journey-section rect').forEach((el) => {
5851
+ setFill(el, t.vars.quadrantFill);
5852
+ setStroke(el, t.vars.border);
5853
+ });
5854
+
5855
+ // ── 11. Gantt ──
5856
+ svg.querySelectorAll('rect.task, rect.task0, rect.task1, rect.task2, rect.task3, rect.activeTask0, rect.activeTask1, rect.activeTask2, rect.activeTask3').forEach((el, i) => {
5857
+ setFill(el, cyclePalette(i));
5858
+ setStroke(el, 'none');
5859
+ });
5860
+ svg.querySelectorAll('rect.done, rect.done0, rect.done1, rect.done2, rect.done3, rect.doneTask').forEach((el) => {
5861
+ setFill(el, t.vars.inner);
5862
+ setStroke(el, t.vars.border);
5863
+ });
5864
+ svg.querySelectorAll('rect.crit, rect.crit0, rect.crit1, rect.crit2, rect.crit3').forEach((el) => {
5865
+ setFill(el, t.vars.accent);
5866
+ setStroke(el, t.vars.accent);
5867
+ });
5868
+ svg.querySelectorAll('.taskText, .taskTextOutsideRight, .taskTextOutsideLeft, text.taskText').forEach((el) => setFill(el, t.vars.titleFill));
5869
+ svg.querySelectorAll('.tick text, g.tick text').forEach((el) => setFill(el, t.vars.axisText));
5870
+ svg.querySelectorAll('.tick line, .grid line, g.tick line, g.grid line').forEach((el) => setStroke(el, t.vars.inner));
5871
+ svg.querySelectorAll('.section0 text, .section1 text, .section2 text, .section3 text, .sectionTitle0, .sectionTitle1, .sectionTitle2, .sectionTitle3').forEach((el) => setFill(el, t.vars.axisText));
5872
+ svg.querySelectorAll('line.today, .today').forEach((el) => setStroke(el, t.vars.accent));
5873
+
5874
+ // ── 12. Timeline ──
5875
+ svg.querySelectorAll('.timeline rect, .timeline-section rect').forEach((el, i) => {
5876
+ setFill(el, cyclePalette(i));
5877
+ setStroke(el, t.vars.border);
5878
+ });
5879
+ svg.querySelectorAll('.timeline text, .timeline foreignObject *').forEach((el) => {
5880
+ setFill(el, t.vars.titleFill);
5881
+ setColor(el, t.vars.titleFill);
5882
+ });
5883
+ svg.querySelectorAll('.timeline path, .timeline-line').forEach((el) => {
5884
+ setStroke(el, t.vars.border);
5885
+ setFill(el, 'none');
5886
+ });
5887
+
5888
+ // ── 13. Git graph ──
5889
+ svg.querySelectorAll('.commit-bullets circle, circle.commit, .commit').forEach((el, i) => {
5890
+ setFill(el, cyclePalette(i));
5891
+ setStroke(el, t.vars.border);
5892
+ });
5893
+ svg.querySelectorAll('.commit-arrows path, path.commit-arrow, .commit-arrow').forEach((el) => {
5894
+ setStroke(el, t.vars.border);
5895
+ setFill(el, 'none');
5896
+ });
5897
+ svg.querySelectorAll('text.commit-label, .commit-label, .branch-label').forEach((el) => setFill(el, t.vars.titleFill));
5898
+
5899
+ // ── 14. Generic title (every chart type) ──
5900
+ svg.querySelectorAll('text.titleText, .titleText, text.chart-title').forEach((el) => setFill(el, t.vars.titleFill));
5901
+
5902
+ // ── 15. Universal text safety net ──
5903
+ // Any text element that survived the per-type passes
5904
+ // without an inline style fill — pin to titleFill so no
5905
+ // text leaks the mermaid-default purple/blue.
5906
+ svg.querySelectorAll('text').forEach((el) => {
5907
+ if (!el.style.fill) setFill(el, t.vars.titleFill);
5908
+ });
5909
+
5910
+ // ── 16. Mark the parent pre.mermaid as painted ──
5911
+ // CSS hides `.body pre.mermaid svg` until `.is-painted`
5912
+ // is added on the parent. We add it here so that even
5913
+ // when the SVG is later inserted into the live DOM, the
5914
+ // CSS-based hide is already lifted (no flash).
5915
+ const host = svg.closest('pre.mermaid');
5916
+ if (host && !host.classList.contains('is-painted')) {
5917
+ host.classList.add('is-painted');
5918
+ }
5919
+ } catch (forceColorErr) {
5920
+ console.warn('[mermaid] palette takeover failed for svg:', forceColorErr);
5921
+ }
5922
+ }; // applySpineToSvg
5923
+
5924
+ // Wrapper · walks every onscreen SVG and applies the
5925
+ // takeover. Used by the safety polling + MutationObserver
5926
+ // (defense-in-depth) for any SVGs that were already
5927
+ // inserted before the offscreen-render loop took over.
5928
+ const applySpinePaletteOnce = () => {
5929
+ document.querySelectorAll('.mermaid svg').forEach(applySpineToSvg);
5930
+ };
5931
+
5932
+ // ── Offscreen render via mermaid.run + DOM relocation ─
5933
+ // DEFINITIVE fix for "first load wrong / strict
5934
+ // alternation." Keep mermaid.run (it handles lazy module
5935
+ // loading for diagram types correctly — mermaid.render's
5936
+ // first call races with that loading on cold cache),
5937
+ // but move every pre.mermaid INTO AN OFFSCREEN CONTAINER
5938
+ // before running. mermaid renders inside the offscreen
5939
+ // container (user can't see it), we apply the spine
5940
+ // takeover to every resulting SVG, then move each
5941
+ // pre.mermaid back to its original DOM position. The
5942
+ // user's first paint of each chart shows the
5943
+ // spine-coloured SVG; no race possible.
5944
+ //
5945
+ // Diagnostic logs (console.info) so the user can verify
5946
+ // which path was taken and at what timing — open DevTools
5947
+ // and refresh; the log narrates the flow.
5948
+ const _mLog = (msg, ...rest) => {
5949
+ try { console.info(`[mermaid-spine] ${msg}`, ...rest); } catch (_) {}
5950
+ };
5951
+ _mLog('flow start · spine =', spineKey);
5952
+ const charts = Array.from(document.querySelectorAll('pre.mermaid'));
5953
+ _mLog('charts found:', charts.length);
5954
+ if (charts.length > 0) {
5955
+ // Capture original DOM positions so we can restore each
5956
+ // pre.mermaid exactly where it was.
5957
+ const anchors = charts.map((el) => ({
5958
+ el,
5959
+ parent: el.parentNode,
5960
+ nextSibling: el.nextSibling,
5961
+ }));
5962
+ // Build the offscreen host. Width matches a typical
5963
+ // article column so mermaid sizes the SVG correctly
5964
+ // (useMaxWidth: true uses the offscreen container's
5965
+ // dimensions for layout). Height: 0 + overflow: visible
5966
+ // so mermaid can compute proper SVG sizes.
5967
+ const offscreen = document.createElement('div');
5968
+ offscreen.id = 'mermaid-offscreen-host';
5969
+ offscreen.style.cssText = [
5970
+ 'position: absolute',
5971
+ 'left: -99999px',
5972
+ 'top: 0',
5973
+ 'width: 880px',
5974
+ 'visibility: hidden',
5975
+ 'pointer-events: none',
5976
+ 'z-index: -1',
5977
+ ].join('; ');
5978
+ document.body.appendChild(offscreen);
5979
+ // Move each pre.mermaid into the offscreen host.
5980
+ anchors.forEach((a) => offscreen.appendChild(a.el));
5981
+ _mLog('moved offscreen, calling mermaid.run');
5982
+ // Run mermaid · scoped to the offscreen host so it only
5983
+ // touches elements we just moved. mermaid handles lazy
5984
+ // module registration internally.
5985
+ try {
5986
+ await mermaid.run({ querySelector: '#mermaid-offscreen-host pre.mermaid' });
5987
+ _mLog('mermaid.run resolved');
5988
+ } catch (runErr) {
5989
+ console.warn('[mermaid-spine] offscreen run failed:', runErr);
5990
+ }
5991
+ // Wait for mermaid's deferred post-render work to land.
5992
+ // Quadrant chart (specifically) does d3-based label
5993
+ // positioning in a setTimeout(0) AFTER mermaid.run
5994
+ // resolves — if we apply takeover before that fires,
5995
+ // mermaid overwrites our fills. Two rAF + setTimeout(0)
5996
+ // cycles guarantee deferred work has settled. Cheap
5997
+ // (~32ms) and only runs once per page load.
5998
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(r, 0))));
5999
+ // Apply takeover to all SVGs while still offscreen.
6000
+ const svgsOffscreen = offscreen.querySelectorAll('svg');
6001
+ _mLog('SVGs rendered offscreen:', svgsOffscreen.length);
6002
+ svgsOffscreen.forEach(applySpineToSvg);
6003
+ _mLog('takeover applied offscreen');
6004
+ // Move each pre.mermaid back to its original position
6005
+ // and mark as painted (CSS reveal rule). Single DOM
6006
+ // mutation per chart → single paint frame with the
6007
+ // corrected SVG already in place.
6008
+ anchors.forEach((a) => {
6009
+ a.el.classList.add('is-painted');
6010
+ if (a.parent) {
6011
+ if (a.nextSibling && a.nextSibling.parentNode === a.parent) {
6012
+ a.parent.insertBefore(a.el, a.nextSibling);
6013
+ } else {
6014
+ a.parent.appendChild(a.el);
5460
6015
  }
5461
6016
  }
6017
+ // Re-apply takeover immediately after move · catches
6018
+ // any post-attach mermaid behaviour (ResizeObserver
6019
+ // re-renders, deferred rAF) that might restyle.
6020
+ a.el.querySelectorAll('svg').forEach(applySpineToSvg);
5462
6021
  });
5463
- } catch (forceColorErr) {
5464
- console.warn('[mermaid] post-render colour force failed:', forceColorErr);
6022
+ _mLog('moved back onscreen + reapplied takeover');
6023
+ // Cleanup offscreen host.
6024
+ offscreen.remove();
5465
6025
  }
6026
+
6027
+ // Safety net · if the takeover errors out for some reason
6028
+ // (script bug, exotic chart type), make sure every chart
6029
+ // becomes visible within 6s so the user at least sees the
6030
+ // chart (even if colours leak) instead of an empty frame.
6031
+ // Normal flow: each successful takeover pass marks its
6032
+ // pre.mermaid as `.is-painted` immediately, so the chart
6033
+ // appears as soon as the first pass succeeds (~16ms).
6034
+ setTimeout(() => {
6035
+ document.querySelectorAll('pre.mermaid:not(.is-painted)').forEach((el) => {
6036
+ el.classList.add('is-painted');
6037
+ });
6038
+ }, 6000);
6039
+
6040
+ // ── Continuous re-apply for the first 5 seconds ──
6041
+ // The user reported strict alternation ("1st wrong, 2nd
6042
+ // right, 3rd wrong, 4th right…") which means a single
6043
+ // setTimeout chain wasn't enough — mermaid's deferred
6044
+ // work was landing in different timing windows on
6045
+ // alternate loads, and my chain was missing odd-numbered
6046
+ // ones.
6047
+ //
6048
+ // Defense in depth: re-apply EVERY 60ms for the first 5
6049
+ // seconds. By the time the user perceives the chart, our
6050
+ // styles have been pinned 80+ times across every possible
6051
+ // mermaid post-render window. After 5s, switch to a pure
6052
+ // MutationObserver (only fires on real DOM changes) so we
6053
+ // keep up with future mutations without the polling cost.
6054
+ //
6055
+ // Each call is idempotent — same selectors set the same
6056
+ // inline-style values, so re-running is a cheap no-op
6057
+ // when nothing changed.
6058
+ applySpinePaletteOnce(); // sync, before next paint
6059
+ let pollCount = 0;
6060
+ const pollInterval = setInterval(() => {
6061
+ applySpinePaletteOnce();
6062
+ if (++pollCount > 80) clearInterval(pollInterval); // 80 × 60ms ≈ 4.8s
6063
+ }, 60);
6064
+
6065
+ // Persistent MutationObserver · catches any post-poll
6066
+ // mutations (container resize, font swap, late SSE
6067
+ // re-render). Watches structural changes only — our own
6068
+ // setProperty calls modify the `style` attribute, which
6069
+ // is excluded by `attributes: false` (default), so this
6070
+ // can't loop on itself.
6071
+ document.querySelectorAll('.mermaid svg').forEach((svg) => {
6072
+ const obs = new MutationObserver(() => {
6073
+ requestAnimationFrame(applySpinePaletteOnce);
6074
+ });
6075
+ obs.observe(svg, { childList: true, subtree: true });
6076
+ });
5466
6077
  // ── Post-render xychart bar slimming ─────────────────────
5467
6078
  // mermaid xychart-beta renders bars at near-full slot width
5468
6079
  // regardless of `plotReservedSpacePercent` in some 11.x