privateboard 0.1.5 → 0.1.7
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 +12 -2
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
- package/public/agent-profile.js +14 -0
- package/public/app.js +465 -704
- package/public/home.html +3 -3
- package/public/index.html +193 -151
- package/public/report.html +356 -59
- package/public/typing-sfx.js +158 -0
- package/public/user-settings.css +90 -0
- package/public/user-settings.js +71 -0
package/public/report.html
CHANGED
|
@@ -588,6 +588,29 @@
|
|
|
588
588
|
border-top-width: 0;
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
+
/* ─── Hide mermaid SVG until spine palette is applied ─────────
|
|
592
|
+
Pattern from user reports: "first time wrong, refresh OK" /
|
|
593
|
+
strict alternation (1st wrong, 2nd right, 3rd wrong, 4th
|
|
594
|
+
right). Cause: even with our continuous post-render takeover
|
|
595
|
+
(see report.html post-mermaid.run block), mermaid paints at
|
|
596
|
+
least once with its default palette BEFORE our takeover lands
|
|
597
|
+
— and the user sees that first paint. The takeover then
|
|
598
|
+
corrects it, but the user's already perceived "wrong colors."
|
|
599
|
+
On refresh, cached rendering may finish faster and the first
|
|
600
|
+
paint happens AFTER our takeover, which they perceive as
|
|
601
|
+
correct.
|
|
602
|
+
Defense: hide the SVG inside each pre.mermaid until we add
|
|
603
|
+
the `is-painted` class. The post-render takeover adds the
|
|
604
|
+
class as its last step. Until then, the user sees the frame
|
|
605
|
+
(matching the spine) with no SVG inside — no off-brand
|
|
606
|
+
colors flash at any point. */
|
|
607
|
+
.body pre.mermaid svg {
|
|
608
|
+
visibility: hidden;
|
|
609
|
+
}
|
|
610
|
+
.body pre.mermaid.is-painted svg {
|
|
611
|
+
visibility: visible;
|
|
612
|
+
}
|
|
613
|
+
|
|
591
614
|
/* ─── Metric-strip · spine-agnostic baseline ─────────────────────
|
|
592
615
|
Editorial / Swiss register: oversized thin numerals top-left, mute
|
|
593
616
|
mono label bottom-left, hairline grid (no card backgrounds, no
|
|
@@ -5396,73 +5419,347 @@
|
|
|
5396
5419
|
},
|
|
5397
5420
|
});
|
|
5398
5421
|
await mermaid.run({ querySelector: ".mermaid" });
|
|
5399
|
-
// ── Post-render
|
|
5400
|
-
//
|
|
5401
|
-
//
|
|
5402
|
-
//
|
|
5403
|
-
//
|
|
5404
|
-
//
|
|
5405
|
-
//
|
|
5406
|
-
//
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5422
|
+
// ── Post-render spine palette takeover ───────────────────
|
|
5423
|
+
// Definitive fix for "mermaid charts don't match the spine
|
|
5424
|
+
// palette." Mermaid's color system has 4 layers (theme
|
|
5425
|
+
// preset, themeVariables, embedded SVG <style>, inline
|
|
5426
|
+
// attributes) that all leak in different ways. Instead of
|
|
5427
|
+
// playing whack-a-mole with mermaid's internals, we let
|
|
5428
|
+
// mermaid render the structure, then:
|
|
5429
|
+
// 1. WIPE mermaid's embedded <style> block in each SVG
|
|
5430
|
+
// (eliminates the CSS-vs-attribute specificity gotcha
|
|
5431
|
+
// where <style> rules silently beat setAttribute calls)
|
|
5432
|
+
// 2. Force-set fill/stroke via inline `style` attribute on
|
|
5433
|
+
// every visual element by class — inline style has the
|
|
5434
|
+
// highest CSS specificity, so nothing in the cascade
|
|
5435
|
+
// can override our spine palette.
|
|
5436
|
+
// Result: every chart element renders in the spine's colors
|
|
5437
|
+
// regardless of mermaid version, theme preset, or which
|
|
5438
|
+
// themeVariables we pass.
|
|
5439
|
+
// Define the takeover as a re-runnable function so we can
|
|
5440
|
+
// schedule it multiple times to defeat first-load timing
|
|
5441
|
+
// races. Pattern observed: on cold loads (CDN uncached, CSS
|
|
5442
|
+
// uncached) mermaid does async post-render work via
|
|
5443
|
+
// requestAnimationFrame / setTimeout AFTER the run() promise
|
|
5444
|
+
// resolves — re-injecting <style>, recomputing layouts, etc.
|
|
5445
|
+
// A single sync takeover then loses the race because mermaid
|
|
5446
|
+
// overwrites our inline styles afterward. On refresh
|
|
5447
|
+
// everything's cached, mermaid finishes faster, and a single
|
|
5448
|
+
// takeover wins by accident.
|
|
5449
|
+
//
|
|
5450
|
+
// Fix: run the takeover at multiple frames + delays so any
|
|
5451
|
+
// mermaid post-processing gets caught regardless of timing.
|
|
5452
|
+
// Each call is idempotent (same selectors set the same
|
|
5453
|
+
// inline styles) so re-running is cheap.
|
|
5454
|
+
const setFill = (el, c) => { if (c) el.style.setProperty('fill', c, 'important'); };
|
|
5455
|
+
const setStroke = (el, c) => { if (c !== undefined && c !== null) el.style.setProperty('stroke', c, 'important'); };
|
|
5456
|
+
const setColor = (el, c) => { if (c) el.style.setProperty('color', c, 'important'); };
|
|
5457
|
+
const cyclePalette = (i) => t.palette[i % t.palette.length];
|
|
5458
|
+
|
|
5459
|
+
const applySpinePaletteOnce = () => {
|
|
5460
|
+
try {
|
|
5461
|
+
document.querySelectorAll('.mermaid svg').forEach((svg) => {
|
|
5462
|
+
// ── 1. Wipe mermaid's embedded <style> block ──
|
|
5463
|
+
// Mermaid generates `<style>.quadrant-chart > .quadrant-
|
|
5464
|
+
// point > circle { fill: #...; }` etc. inside each SVG;
|
|
5465
|
+
// those CSS rules beat our setAttribute-set fills. After
|
|
5466
|
+
// removal, our inline-style fills below have no rivals.
|
|
5467
|
+
svg.querySelectorAll(':scope > style, defs > style').forEach((s) => s.remove());
|
|
5468
|
+
|
|
5469
|
+
// ── 2. SVG canvas / background ──
|
|
5470
|
+
// Any rect that fills the whole SVG is the chart canvas;
|
|
5471
|
+
// pin to t.vars.background so the chart blends with its
|
|
5472
|
+
// spine frame instead of showing mermaid's default white.
|
|
5473
|
+
const sw = parseFloat(svg.getAttribute('width') || '0');
|
|
5474
|
+
svg.querySelectorAll(':scope > rect, :scope > g > rect.background, rect.main-bkg').forEach((el) => {
|
|
5475
|
+
const w = parseFloat(el.getAttribute('width') || '0');
|
|
5476
|
+
if (w > 0 && sw > 0 && w >= sw * 0.95) setFill(el, t.vars.background);
|
|
5415
5477
|
});
|
|
5416
|
-
|
|
5417
|
-
//
|
|
5418
|
-
//
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
el.setAttribute('fill', t.vars.pointFill);
|
|
5434
|
-
el.setAttribute('stroke', t.vars.background);
|
|
5478
|
+
|
|
5479
|
+
// ── 3. Quadrant chart ──
|
|
5480
|
+
// Mermaid 10's actual class names (verified against rendered DOM):
|
|
5481
|
+
// `.quadrant` — each of the 4 quadrant rects (NOT .quadrant-fill)
|
|
5482
|
+
// `.quadrants` — parent group
|
|
5483
|
+
// `.data-point` — each data circle (NOT .quadrant-point)
|
|
5484
|
+
// `.data-points` — parent group
|
|
5485
|
+
// `.axis-line` / `.axisl-line` — axis lines
|
|
5486
|
+
// `.bottom-axis`, `.left-axis` — axis text
|
|
5487
|
+
// Older / alternate class names from .quadrant-* family kept
|
|
5488
|
+
// as a defensive fallback for version drift.
|
|
5489
|
+
svg.querySelectorAll('rect.quadrant, .quadrants > .quadrant, rect.quadrant-fill, .quadrant-fill').forEach((el) => setFill(el, t.vars.quadrantFill));
|
|
5490
|
+
svg.querySelectorAll('.axis-line, .axisl-line, .quadrants line').forEach((el) => setStroke(el, t.vars.border));
|
|
5491
|
+
svg.querySelectorAll('.quadrant-internal-border, line.quadrant-internal-border').forEach((el) => setStroke(el, t.vars.inner));
|
|
5492
|
+
svg.querySelectorAll('.quadrant-external-border, rect.quadrant-external-border').forEach((el) => {
|
|
5493
|
+
setStroke(el, t.vars.border);
|
|
5494
|
+
setFill(el, 'none');
|
|
5435
5495
|
});
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5496
|
+
svg.querySelectorAll('text.quadrant-title, .quadrant-title, .chart-title').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5497
|
+
// Quadrant labels (the 4 corner labels). Mermaid 10
|
|
5498
|
+
// doesn't put them in a class; they're plain `<text>` inside
|
|
5499
|
+
// each `.quadrant` group. Walk children of `.quadrant` text.
|
|
5500
|
+
svg.querySelectorAll('.quadrant text, .quadrants text:not(.chart-title)').forEach((el) => setFill(el, t.vars.quadrantText));
|
|
5501
|
+
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));
|
|
5502
|
+
// Axis labels · "Low likelihood / High likelihood" etc.
|
|
5503
|
+
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));
|
|
5504
|
+
// Data points · the load-bearing accent.
|
|
5505
|
+
svg.querySelectorAll('.data-point, .data-points circle, circle.data-point, .quadrant-point > circle, g.quadrant-point > circle, circle.quadrant-point').forEach((el) => {
|
|
5506
|
+
setFill(el, t.vars.pointFill);
|
|
5507
|
+
setStroke(el, t.vars.background);
|
|
5508
|
+
});
|
|
5509
|
+
svg.querySelectorAll('.data-point text, .data-points text, text.point-text, .quadrant-point text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5510
|
+
|
|
5511
|
+
// ── 4. Pie chart ──
|
|
5512
|
+
svg.querySelectorAll('path.pieCircle, g.pieCircle path, .slice').forEach((el, i) => {
|
|
5513
|
+
setFill(el, cyclePalette(i));
|
|
5514
|
+
setStroke(el, t.vars.background);
|
|
5440
5515
|
});
|
|
5441
|
-
// Pie outer ring border.
|
|
5442
5516
|
svg.querySelectorAll('.pieOuterCircle, circle.pieOuterCircle').forEach((el) => {
|
|
5443
|
-
el
|
|
5444
|
-
el
|
|
5517
|
+
setStroke(el, t.vars.border);
|
|
5518
|
+
setFill(el, 'none');
|
|
5519
|
+
});
|
|
5520
|
+
svg.querySelectorAll('text.slice, text.pieLegendText, .legend text, g.legend text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5521
|
+
svg.querySelectorAll('.pieTitleText').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5522
|
+
|
|
5523
|
+
// ── 5. xychart-beta ──
|
|
5524
|
+
// Bars cycle through palette; axes + grid + labels match
|
|
5525
|
+
// the spine's neutrals.
|
|
5526
|
+
svg.querySelectorAll('g.plot rect, g.bar-plot rect, g[class*="-plot"] rect, rect.bar').forEach((el, i) => {
|
|
5527
|
+
// Skip the plot-background rect (full plot width).
|
|
5528
|
+
const w = parseFloat(el.getAttribute('width') || '0');
|
|
5529
|
+
if (w >= 200) return;
|
|
5530
|
+
setFill(el, cyclePalette(i));
|
|
5531
|
+
setStroke(el, 'none');
|
|
5532
|
+
});
|
|
5533
|
+
svg.querySelectorAll('.xy-chart .background, rect.background').forEach((el) => setFill(el, 'transparent'));
|
|
5534
|
+
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));
|
|
5535
|
+
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));
|
|
5536
|
+
svg.querySelectorAll('.xy-chart .grid line, .xy-chart line.grid').forEach((el) => setStroke(el, t.vars.inner));
|
|
5537
|
+
|
|
5538
|
+
// ── 6. Flowchart ──
|
|
5539
|
+
svg.querySelectorAll('.flowchart .node rect, .flowchart .node circle, .flowchart .node ellipse, .flowchart .node polygon, .node rect, .node circle, .node polygon, .node ellipse').forEach((el) => {
|
|
5540
|
+
setFill(el, t.vars.quadrantFill);
|
|
5541
|
+
setStroke(el, t.vars.border);
|
|
5542
|
+
});
|
|
5543
|
+
svg.querySelectorAll('.flowchart-link, .edgePath path, .edgePath .path, path.flowchart-link').forEach((el) => {
|
|
5544
|
+
setStroke(el, t.vars.border);
|
|
5545
|
+
setFill(el, 'none');
|
|
5546
|
+
});
|
|
5547
|
+
svg.querySelectorAll('marker path, #arrowhead path, marker#arrowhead path').forEach((el) => {
|
|
5548
|
+
setFill(el, t.vars.border);
|
|
5549
|
+
setStroke(el, t.vars.border);
|
|
5445
5550
|
});
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
el
|
|
5551
|
+
svg.querySelectorAll('.cluster rect').forEach((el) => {
|
|
5552
|
+
setFill(el, t.vars.background);
|
|
5553
|
+
setStroke(el, t.vars.border);
|
|
5449
5554
|
});
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5555
|
+
svg.querySelectorAll('.cluster text, .nodeLabel, .label foreignObject *').forEach((el) => {
|
|
5556
|
+
setFill(el, t.vars.titleFill);
|
|
5557
|
+
setColor(el, t.vars.titleFill);
|
|
5558
|
+
});
|
|
5559
|
+
svg.querySelectorAll('.edgeLabel, .edgeLabel foreignObject *').forEach((el) => {
|
|
5560
|
+
setFill(el, t.vars.axisText);
|
|
5561
|
+
setColor(el, t.vars.axisText);
|
|
5562
|
+
el.style.setProperty('background-color', t.vars.background, 'important');
|
|
5563
|
+
});
|
|
5564
|
+
|
|
5565
|
+
// ── 7. Mindmap ──
|
|
5566
|
+
svg.querySelectorAll('.mindmap g.section circle, .mindmap-node circle, g.mindmap-node circle').forEach((el, i) => {
|
|
5567
|
+
setFill(el, cyclePalette(i));
|
|
5568
|
+
setStroke(el, t.vars.border);
|
|
5569
|
+
});
|
|
5570
|
+
svg.querySelectorAll('.mindmap g.section rect, .mindmap-node rect, g.mindmap-node rect').forEach((el, i) => {
|
|
5571
|
+
setFill(el, cyclePalette(i));
|
|
5572
|
+
setStroke(el, t.vars.border);
|
|
5573
|
+
});
|
|
5574
|
+
svg.querySelectorAll('.mindmap g.section polygon, .mindmap-node polygon, g.mindmap-node polygon').forEach((el, i) => {
|
|
5575
|
+
setFill(el, cyclePalette(i));
|
|
5576
|
+
setStroke(el, t.vars.border);
|
|
5577
|
+
});
|
|
5578
|
+
svg.querySelectorAll('.mindmap-edges path, .mindmap path.edge').forEach((el) => {
|
|
5579
|
+
setStroke(el, t.vars.border);
|
|
5580
|
+
setFill(el, 'none');
|
|
5581
|
+
});
|
|
5582
|
+
svg.querySelectorAll('.mindmap text, .mindmap-node text, g.mindmap-node text, .mindmap foreignObject *').forEach((el) => {
|
|
5583
|
+
setFill(el, t.vars.titleFill);
|
|
5584
|
+
setColor(el, t.vars.titleFill);
|
|
5585
|
+
});
|
|
5586
|
+
|
|
5587
|
+
// ── 8. Sequence diagram ──
|
|
5588
|
+
svg.querySelectorAll('rect.actor, .actor').forEach((el) => {
|
|
5589
|
+
setFill(el, t.vars.quadrantFill);
|
|
5590
|
+
setStroke(el, t.vars.border);
|
|
5591
|
+
});
|
|
5592
|
+
svg.querySelectorAll('line.actor-line, .actor-line').forEach((el) => setStroke(el, t.vars.border));
|
|
5593
|
+
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));
|
|
5594
|
+
svg.querySelectorAll('.messageLine0, .messageLine1, line.messageLine0, line.messageLine1').forEach((el) => setStroke(el, t.vars.titleFill));
|
|
5595
|
+
svg.querySelectorAll('rect.note, .note').forEach((el) => {
|
|
5596
|
+
setFill(el, t.vars.accentSoft);
|
|
5597
|
+
setStroke(el, t.vars.border);
|
|
5598
|
+
});
|
|
5599
|
+
svg.querySelectorAll('.activation0, .activation1, .activation2, rect.activation0, rect.activation1, rect.activation2').forEach((el) => {
|
|
5600
|
+
setFill(el, t.vars.inner);
|
|
5601
|
+
setStroke(el, t.vars.border);
|
|
5602
|
+
});
|
|
5603
|
+
svg.querySelectorAll('.labelBox, rect.labelBox').forEach((el) => {
|
|
5604
|
+
setFill(el, t.vars.quadrantFill);
|
|
5605
|
+
setStroke(el, t.vars.border);
|
|
5606
|
+
});
|
|
5607
|
+
|
|
5608
|
+
// ── 9. State diagram ──
|
|
5609
|
+
svg.querySelectorAll('.stateGroup rect, g.stateGroup rect, rect.state').forEach((el) => {
|
|
5610
|
+
setFill(el, t.vars.quadrantFill);
|
|
5611
|
+
setStroke(el, t.vars.border);
|
|
5612
|
+
});
|
|
5613
|
+
svg.querySelectorAll('.stateGroup circle, g.stateGroup circle').forEach((el) => {
|
|
5614
|
+
setFill(el, t.vars.titleFill);
|
|
5615
|
+
setStroke(el, t.vars.titleFill);
|
|
5616
|
+
});
|
|
5617
|
+
svg.querySelectorAll('.stateGroup line, line.transition, path.transition').forEach((el) => {
|
|
5618
|
+
setStroke(el, t.vars.border);
|
|
5619
|
+
setFill(el, 'none');
|
|
5620
|
+
});
|
|
5621
|
+
svg.querySelectorAll('.stateLabel, .state-description, .stateGroup text').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5622
|
+
svg.querySelectorAll('.compositeBackground, rect.compositeBackground').forEach((el) => {
|
|
5623
|
+
setFill(el, t.vars.background);
|
|
5624
|
+
setStroke(el, t.vars.border);
|
|
5625
|
+
});
|
|
5626
|
+
|
|
5627
|
+
// ── 10. Journey ──
|
|
5628
|
+
svg.querySelectorAll('.face-circle, circle.face-circle, .journey-section .face').forEach((el, i) => {
|
|
5629
|
+
setFill(el, cyclePalette(i));
|
|
5630
|
+
setStroke(el, t.vars.background);
|
|
5631
|
+
});
|
|
5632
|
+
svg.querySelectorAll('.journey-section text, .section text, .journey-section foreignObject *').forEach((el) => {
|
|
5633
|
+
setFill(el, t.vars.titleFill);
|
|
5634
|
+
setColor(el, t.vars.titleFill);
|
|
5635
|
+
});
|
|
5636
|
+
svg.querySelectorAll('rect.journey, .journey-section rect').forEach((el) => {
|
|
5637
|
+
setFill(el, t.vars.quadrantFill);
|
|
5638
|
+
setStroke(el, t.vars.border);
|
|
5639
|
+
});
|
|
5640
|
+
|
|
5641
|
+
// ── 11. Gantt ──
|
|
5642
|
+
svg.querySelectorAll('rect.task, rect.task0, rect.task1, rect.task2, rect.task3, rect.activeTask0, rect.activeTask1, rect.activeTask2, rect.activeTask3').forEach((el, i) => {
|
|
5643
|
+
setFill(el, cyclePalette(i));
|
|
5644
|
+
setStroke(el, 'none');
|
|
5645
|
+
});
|
|
5646
|
+
svg.querySelectorAll('rect.done, rect.done0, rect.done1, rect.done2, rect.done3, rect.doneTask').forEach((el) => {
|
|
5647
|
+
setFill(el, t.vars.inner);
|
|
5648
|
+
setStroke(el, t.vars.border);
|
|
5649
|
+
});
|
|
5650
|
+
svg.querySelectorAll('rect.crit, rect.crit0, rect.crit1, rect.crit2, rect.crit3').forEach((el) => {
|
|
5651
|
+
setFill(el, t.vars.accent);
|
|
5652
|
+
setStroke(el, t.vars.accent);
|
|
5653
|
+
});
|
|
5654
|
+
svg.querySelectorAll('.taskText, .taskTextOutsideRight, .taskTextOutsideLeft, text.taskText').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5655
|
+
svg.querySelectorAll('.tick text, g.tick text').forEach((el) => setFill(el, t.vars.axisText));
|
|
5656
|
+
svg.querySelectorAll('.tick line, .grid line, g.tick line, g.grid line').forEach((el) => setStroke(el, t.vars.inner));
|
|
5657
|
+
svg.querySelectorAll('.section0 text, .section1 text, .section2 text, .section3 text, .sectionTitle0, .sectionTitle1, .sectionTitle2, .sectionTitle3').forEach((el) => setFill(el, t.vars.axisText));
|
|
5658
|
+
svg.querySelectorAll('line.today, .today').forEach((el) => setStroke(el, t.vars.accent));
|
|
5659
|
+
|
|
5660
|
+
// ── 12. Timeline ──
|
|
5661
|
+
svg.querySelectorAll('.timeline rect, .timeline-section rect').forEach((el, i) => {
|
|
5662
|
+
setFill(el, cyclePalette(i));
|
|
5663
|
+
setStroke(el, t.vars.border);
|
|
5664
|
+
});
|
|
5665
|
+
svg.querySelectorAll('.timeline text, .timeline foreignObject *').forEach((el) => {
|
|
5666
|
+
setFill(el, t.vars.titleFill);
|
|
5667
|
+
setColor(el, t.vars.titleFill);
|
|
5668
|
+
});
|
|
5669
|
+
svg.querySelectorAll('.timeline path, .timeline-line').forEach((el) => {
|
|
5670
|
+
setStroke(el, t.vars.border);
|
|
5671
|
+
setFill(el, 'none');
|
|
5672
|
+
});
|
|
5673
|
+
|
|
5674
|
+
// ── 13. Git graph ──
|
|
5675
|
+
svg.querySelectorAll('.commit-bullets circle, circle.commit, .commit').forEach((el, i) => {
|
|
5676
|
+
setFill(el, cyclePalette(i));
|
|
5677
|
+
setStroke(el, t.vars.border);
|
|
5678
|
+
});
|
|
5679
|
+
svg.querySelectorAll('.commit-arrows path, path.commit-arrow, .commit-arrow').forEach((el) => {
|
|
5680
|
+
setStroke(el, t.vars.border);
|
|
5681
|
+
setFill(el, 'none');
|
|
5682
|
+
});
|
|
5683
|
+
svg.querySelectorAll('text.commit-label, .commit-label, .branch-label').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5684
|
+
|
|
5685
|
+
// ── 14. Generic title (every chart type) ──
|
|
5686
|
+
svg.querySelectorAll('text.titleText, .titleText, text.chart-title').forEach((el) => setFill(el, t.vars.titleFill));
|
|
5687
|
+
|
|
5688
|
+
// ── 15. Universal text safety net ──
|
|
5689
|
+
// Any text element that survived the per-type passes
|
|
5690
|
+
// without an inline style fill — pin to titleFill so no
|
|
5691
|
+
// text leaks the mermaid-default purple/blue.
|
|
5692
|
+
svg.querySelectorAll('text').forEach((el) => {
|
|
5693
|
+
if (!el.style.fill) setFill(el, t.vars.titleFill);
|
|
5694
|
+
});
|
|
5695
|
+
|
|
5696
|
+
// ── 16. Mark the parent pre.mermaid as painted ──
|
|
5697
|
+
// CSS hides `.body pre.mermaid svg` until `.is-painted`
|
|
5698
|
+
// is added, so the user never sees mermaid's default
|
|
5699
|
+
// colors during the brief window between mermaid.run()
|
|
5700
|
+
// resolving and our takeover applying. Adding it here
|
|
5701
|
+
// (last step of each takeover pass) reveals the SVG only
|
|
5702
|
+
// after our spine fills are pinned.
|
|
5703
|
+
const host = svg.closest('pre.mermaid');
|
|
5704
|
+
if (host && !host.classList.contains('is-painted')) {
|
|
5705
|
+
host.classList.add('is-painted');
|
|
5461
5706
|
}
|
|
5707
|
+
}); // forEach svg
|
|
5708
|
+
} catch (forceColorErr) {
|
|
5709
|
+
console.warn('[mermaid] post-render palette takeover failed:', forceColorErr);
|
|
5710
|
+
}
|
|
5711
|
+
}; // applySpinePaletteOnce
|
|
5712
|
+
|
|
5713
|
+
// Safety net · if the takeover errors out for some reason
|
|
5714
|
+
// (script bug, exotic chart type), make sure every chart
|
|
5715
|
+
// becomes visible within 6s so the user at least sees the
|
|
5716
|
+
// chart (even if colours leak) instead of an empty frame.
|
|
5717
|
+
// Normal flow: each successful takeover pass marks its
|
|
5718
|
+
// pre.mermaid as `.is-painted` immediately, so the chart
|
|
5719
|
+
// appears as soon as the first pass succeeds (~16ms).
|
|
5720
|
+
setTimeout(() => {
|
|
5721
|
+
document.querySelectorAll('pre.mermaid:not(.is-painted)').forEach((el) => {
|
|
5722
|
+
el.classList.add('is-painted');
|
|
5462
5723
|
});
|
|
5463
|
-
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5724
|
+
}, 6000);
|
|
5725
|
+
|
|
5726
|
+
// ── Continuous re-apply for the first 5 seconds ──
|
|
5727
|
+
// The user reported strict alternation ("1st wrong, 2nd
|
|
5728
|
+
// right, 3rd wrong, 4th right…") which means a single
|
|
5729
|
+
// setTimeout chain wasn't enough — mermaid's deferred
|
|
5730
|
+
// work was landing in different timing windows on
|
|
5731
|
+
// alternate loads, and my chain was missing odd-numbered
|
|
5732
|
+
// ones.
|
|
5733
|
+
//
|
|
5734
|
+
// Defense in depth: re-apply EVERY 60ms for the first 5
|
|
5735
|
+
// seconds. By the time the user perceives the chart, our
|
|
5736
|
+
// styles have been pinned 80+ times across every possible
|
|
5737
|
+
// mermaid post-render window. After 5s, switch to a pure
|
|
5738
|
+
// MutationObserver (only fires on real DOM changes) so we
|
|
5739
|
+
// keep up with future mutations without the polling cost.
|
|
5740
|
+
//
|
|
5741
|
+
// Each call is idempotent — same selectors set the same
|
|
5742
|
+
// inline-style values, so re-running is a cheap no-op
|
|
5743
|
+
// when nothing changed.
|
|
5744
|
+
applySpinePaletteOnce(); // sync, before next paint
|
|
5745
|
+
let pollCount = 0;
|
|
5746
|
+
const pollInterval = setInterval(() => {
|
|
5747
|
+
applySpinePaletteOnce();
|
|
5748
|
+
if (++pollCount > 80) clearInterval(pollInterval); // 80 × 60ms ≈ 4.8s
|
|
5749
|
+
}, 60);
|
|
5750
|
+
|
|
5751
|
+
// Persistent MutationObserver · catches any post-poll
|
|
5752
|
+
// mutations (container resize, font swap, late SSE
|
|
5753
|
+
// re-render). Watches structural changes only — our own
|
|
5754
|
+
// setProperty calls modify the `style` attribute, which
|
|
5755
|
+
// is excluded by `attributes: false` (default), so this
|
|
5756
|
+
// can't loop on itself.
|
|
5757
|
+
document.querySelectorAll('.mermaid svg').forEach((svg) => {
|
|
5758
|
+
const obs = new MutationObserver(() => {
|
|
5759
|
+
requestAnimationFrame(applySpinePaletteOnce);
|
|
5760
|
+
});
|
|
5761
|
+
obs.observe(svg, { childList: true, subtree: true });
|
|
5762
|
+
});
|
|
5466
5763
|
// ── Post-render xychart bar slimming ─────────────────────
|
|
5467
5764
|
// mermaid xychart-beta renders bars at near-full slot width
|
|
5468
5765
|
// regardless of `plotReservedSpacePercent` in some 11.x
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* ────────────────────────────────────────────────────────────────
|
|
2
|
+
typing-sfx.js · Subtle keyboard-click sound effect played as
|
|
3
|
+
directors / chair / brief writer stream their text into the
|
|
4
|
+
chat. Pure synthesised audio · no asset to ship, no autoplay
|
|
5
|
+
policy violations until the user has clicked something.
|
|
6
|
+
|
|
7
|
+
Wire-up:
|
|
8
|
+
· public/index.html loads this before public/app.js.
|
|
9
|
+
· app.js calls window.boardroomTypingSfx.tick() on every
|
|
10
|
+
`message-token` and `brief-token` SSE chunk.
|
|
11
|
+
· The user-settings overlay's User pane carries an enable /
|
|
12
|
+
disable toggle that persists in localStorage.
|
|
13
|
+
|
|
14
|
+
Design choices the comments below justify:
|
|
15
|
+
· Synthesised, not file-based · removes a network round-trip
|
|
16
|
+
and keeps the install slim. The synth is a 40ms decaying
|
|
17
|
+
white-noise burst routed through a bandpass at ~1.8kHz with
|
|
18
|
+
slight per-tick frequency jitter so the cadence doesn't
|
|
19
|
+
fatigue the ear like a metronome.
|
|
20
|
+
· Lazily-created AudioContext · Chrome / Safari refuse to
|
|
21
|
+
resume a context until the page has received a real user
|
|
22
|
+
gesture. We hold off creation until tick() is first called
|
|
23
|
+
AFTER the first interaction; otherwise the first audible
|
|
24
|
+
tick would be a console warning instead of a sound.
|
|
25
|
+
· Throttled to ~12 ticks/sec · token rates of 30-60/s would
|
|
26
|
+
otherwise produce a continuous hiss instead of a typewriter
|
|
27
|
+
cadence. The throttle stays steady regardless of token rate,
|
|
28
|
+
which is the right behaviour: the sound is a presence cue,
|
|
29
|
+
not a literal mapping to byte arrival.
|
|
30
|
+
· Muted while the tab is backgrounded · browsers throttle audio
|
|
31
|
+
there anyway, but explicit muting avoids the rare case where
|
|
32
|
+
a queued AudioBufferSourceNode plays right when the tab
|
|
33
|
+
refocuses.
|
|
34
|
+
──────────────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
(function () {
|
|
37
|
+
const STORAGE_KEY = "boardroom.sfx.typing";
|
|
38
|
+
// Default ON · the user explicitly asked for this feature. They
|
|
39
|
+
// can disable it via the Preference → User pane toggle.
|
|
40
|
+
const DEFAULT_ENABLED = true;
|
|
41
|
+
// Minimum gap between ticks. Set the tick rate; lower = busier.
|
|
42
|
+
// 80ms ≈ 12.5 Hz — fast typist cadence, not a chattering buzz.
|
|
43
|
+
const MIN_TICK_INTERVAL_MS = 80;
|
|
44
|
+
|
|
45
|
+
let _ctx = null;
|
|
46
|
+
let _ctxFailed = false;
|
|
47
|
+
let _hadGesture = false;
|
|
48
|
+
let _enabled = readEnabled();
|
|
49
|
+
let _lastTickAt = 0;
|
|
50
|
+
|
|
51
|
+
function readEnabled() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
54
|
+
if (raw === "on") return true;
|
|
55
|
+
if (raw === "off") return false;
|
|
56
|
+
} catch { /* private mode etc · default-on still applies */ }
|
|
57
|
+
return DEFAULT_ENABLED;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeEnabled(on) {
|
|
61
|
+
try { localStorage.setItem(STORAGE_KEY, on ? "on" : "off"); }
|
|
62
|
+
catch { /* swallow · localStorage may be locked */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Mark that the user has interacted with the page · only after
|
|
66
|
+
* this fires can we safely create / resume an AudioContext.
|
|
67
|
+
* Listeners are passive + once-per-event-type for cheapness. */
|
|
68
|
+
function markGesture() {
|
|
69
|
+
_hadGesture = true;
|
|
70
|
+
// If a context was created pre-gesture (some browsers allow it
|
|
71
|
+
// but leave it suspended), nudge it now.
|
|
72
|
+
if (_ctx && _ctx.state === "suspended") {
|
|
73
|
+
_ctx.resume().catch(() => { /* swallow · keep silent rather than throw */ });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
["pointerdown", "keydown", "touchstart"].forEach((ev) => {
|
|
77
|
+
window.addEventListener(ev, markGesture, { passive: true, capture: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function ensureContext() {
|
|
81
|
+
if (_ctxFailed) return null;
|
|
82
|
+
if (_ctx) return _ctx;
|
|
83
|
+
if (!_hadGesture) return null; // refuse to create until gestured
|
|
84
|
+
try {
|
|
85
|
+
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
86
|
+
if (!Ctx) { _ctxFailed = true; return null; }
|
|
87
|
+
_ctx = new Ctx();
|
|
88
|
+
// If autoplay policy left it suspended even after gesture,
|
|
89
|
+
// resume explicitly · safe to call multiple times.
|
|
90
|
+
if (_ctx.state === "suspended") {
|
|
91
|
+
_ctx.resume().catch(() => { /* swallow */ });
|
|
92
|
+
}
|
|
93
|
+
return _ctx;
|
|
94
|
+
} catch {
|
|
95
|
+
_ctxFailed = true;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tick() {
|
|
101
|
+
if (!_enabled) return;
|
|
102
|
+
if (document.visibilityState !== "visible") return; // background tab
|
|
103
|
+
const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
104
|
+
if (now - _lastTickAt < MIN_TICK_INTERVAL_MS) return;
|
|
105
|
+
const ctx = ensureContext();
|
|
106
|
+
if (!ctx) return;
|
|
107
|
+
_lastTickAt = now;
|
|
108
|
+
|
|
109
|
+
const t0 = ctx.currentTime;
|
|
110
|
+
// 40ms of decaying white noise · the click texture comes from
|
|
111
|
+
// the noise burst itself, the bandpass below shapes its colour.
|
|
112
|
+
const dur = 0.04;
|
|
113
|
+
const buf = ctx.createBuffer(1, Math.max(1, Math.floor(ctx.sampleRate * dur)), ctx.sampleRate);
|
|
114
|
+
const data = buf.getChannelData(0);
|
|
115
|
+
for (let i = 0; i < data.length; i++) {
|
|
116
|
+
// Linear amplitude decay across the buffer · gives the click
|
|
117
|
+
// a natural "tap then fade" envelope before the gain node's
|
|
118
|
+
// own envelope fine-tunes the start.
|
|
119
|
+
data[i] = (Math.random() * 2 - 1) * (1 - i / data.length);
|
|
120
|
+
}
|
|
121
|
+
const noise = ctx.createBufferSource();
|
|
122
|
+
noise.buffer = buf;
|
|
123
|
+
|
|
124
|
+
// Bandpass · keyboard-tap colour is mostly 1.5-3kHz. Jitter the
|
|
125
|
+
// centre frequency a little per tick so consecutive clicks
|
|
126
|
+
// don't sound mechanically identical.
|
|
127
|
+
const bp = ctx.createBiquadFilter();
|
|
128
|
+
bp.type = "bandpass";
|
|
129
|
+
bp.frequency.value = 1700 + Math.random() * 800;
|
|
130
|
+
bp.Q.value = 1.4;
|
|
131
|
+
|
|
132
|
+
// Master envelope · 2ms attack, exponential 50ms decay. Quiet
|
|
133
|
+
// peak (gain ~0.06) so the cue stays under the user's voice
|
|
134
|
+
// floor instead of becoming the loudest thing on screen.
|
|
135
|
+
const gain = ctx.createGain();
|
|
136
|
+
gain.gain.setValueAtTime(0.0001, t0);
|
|
137
|
+
gain.gain.exponentialRampToValueAtTime(0.06, t0 + 0.002);
|
|
138
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.05);
|
|
139
|
+
|
|
140
|
+
noise.connect(bp).connect(gain).connect(ctx.destination);
|
|
141
|
+
noise.start(t0);
|
|
142
|
+
noise.stop(t0 + 0.06);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function setEnabled(on) {
|
|
146
|
+
_enabled = !!on;
|
|
147
|
+
writeEnabled(_enabled);
|
|
148
|
+
// No-op when disabling · live AudioContext stays alive cheaply
|
|
149
|
+
// (a few KB) and an outright close() leaves us re-paying the
|
|
150
|
+
// creation cost if the user toggles back on within the session.
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isEnabled() { return _enabled; }
|
|
154
|
+
|
|
155
|
+
// Public surface · attached to window so app.js (and the
|
|
156
|
+
// user-settings toggle) can reach it without an import.
|
|
157
|
+
window.boardroomTypingSfx = { tick, setEnabled, isEnabled };
|
|
158
|
+
})();
|