🌗 refactor: Consistent Mermaid Theming for Inline and Artifact Renderers (#12055)

* refactor: consistent theming between inline and Artifacts Mermaid Diagram

* refactor: Enhance Mermaid component with improved theming and security features

- Updated Mermaid component to utilize useCallback for performance optimization.
- Increased maximum zoom level from 4 to 10 for better diagram visibility.
- Added security level configuration to Mermaid initialization for enhanced security.
- Refactored theme handling to ensure consistent theming between inline and artifact diagrams.
- Introduced unit tests for Mermaid configuration to validate flowchart settings and theme behavior.

* refactor: Improve theme handling in useMermaid hook

- Enhanced theme variable management by merging custom theme variables with default values for dark mode.
- Ensured consistent theming across Mermaid diagrams by preserving existing theme configurations while applying new defaults.

* refactor: Consolidate imports in mermaid test file

- Combined multiple imports from the mermaid utility into a single statement for improved readability and organization in the test file.

* feat: Add subgraph title contrast adjustment for Mermaid diagrams

- Introduced a utility function to enhance text visibility on subgraph titles by adjusting the fill color based on background luminance.
- Updated the Mermaid component to utilize this function, ensuring better contrast in rendered SVGs.
- Added comprehensive unit tests to validate the contrast adjustment logic across various scenarios.

* refactor: Update MermaidHeader component for improved button accessibility and styling

- Replaced Button components with TooltipAnchor for better accessibility and user experience.
- Consolidated button styles into a single class for consistency.
- Enhanced the layout and spacing of the header for a cleaner appearance.

* fix: hex color handling and improve contrast adjustment in Mermaid diagrams

- Updated hexLuminance function to support 3-character hex shorthand by expanding it to 6 characters.
- Refined the fixSubgraphTitleContrast function to avoid double semicolons in style attributes and ensure proper fill color adjustments based on background luminance.
- Added unit tests to validate the handling of 3-character hex fills and the prevention of double semicolons in text styles.

* chore: Simplify Virtual Scrolling Performance tests by removing performance timing checks

- Removed performance timing checks and associated console logs from tests handling 1000 and 5000 agents.
- Focused tests on verifying the correct rendering of virtual list items without measuring render time.
This commit is contained in:
Danny Avila 2026-03-04 08:25:57 -05:00
parent 6ebee069c7
commit f1eabdbdb7
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
8 changed files with 671 additions and 309 deletions

View file

@ -0,0 +1,172 @@
import {
fixSubgraphTitleContrast,
artifactFlowchartConfig,
inlineFlowchartConfig,
getMermaidFiles,
} from '~/utils/mermaid';
const makeSvg = (clusters: string): Element => {
const parser = new DOMParser();
const doc = parser.parseFromString(
`<svg xmlns="http://www.w3.org/2000/svg">${clusters}</svg>`,
'image/svg+xml',
);
return doc.querySelector('svg')!;
};
describe('mermaid config', () => {
describe('flowchart config invariants', () => {
it('inlineFlowchartConfig must have htmlLabels: false for blob URL <img> rendering', () => {
expect(inlineFlowchartConfig.htmlLabels).toBe(false);
});
it('artifactFlowchartConfig must have htmlLabels: true for direct DOM injection', () => {
expect(artifactFlowchartConfig.htmlLabels).toBe(true);
});
it('both configs share the same base layout settings', () => {
expect(inlineFlowchartConfig.curve).toBe(artifactFlowchartConfig.curve);
expect(inlineFlowchartConfig.nodeSpacing).toBe(artifactFlowchartConfig.nodeSpacing);
expect(inlineFlowchartConfig.rankSpacing).toBe(artifactFlowchartConfig.rankSpacing);
expect(inlineFlowchartConfig.padding).toBe(artifactFlowchartConfig.padding);
});
});
describe('getMermaidFiles', () => {
const content = 'graph TD\n A-->B';
it('produces dark theme files when isDarkMode is true', () => {
const files = getMermaidFiles(content, true);
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"');
expect(files['mermaid.css']).toContain('#212121');
});
it('produces neutral theme files when isDarkMode is false', () => {
const files = getMermaidFiles(content, false);
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "neutral"');
expect(files['mermaid.css']).toContain('#FFFFFF');
});
it('defaults to dark mode when isDarkMode is omitted', () => {
const files = getMermaidFiles(content);
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"');
});
it('includes securityLevel in generated component', () => {
const files = getMermaidFiles(content, true);
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('securityLevel: "strict"');
});
it('includes all required file keys', () => {
const files = getMermaidFiles(content, true);
expect(files['diagram.mmd']).toBe(content);
expect(files['App.tsx']).toBeDefined();
expect(files['index.tsx']).toBeDefined();
expect(files['/components/ui/MermaidDiagram.tsx']).toBeDefined();
expect(files['mermaid.css']).toBeDefined();
});
it('uses artifact flowchart config with htmlLabels: true', () => {
const files = getMermaidFiles(content, true);
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('"htmlLabels": true');
});
it('does not inject custom themeVariables into generated component', () => {
const darkFiles = getMermaidFiles(content, true);
const lightFiles = getMermaidFiles(content, false);
expect(darkFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables');
expect(lightFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables');
});
it('handles empty content', () => {
const files = getMermaidFiles('', true);
expect(files['diagram.mmd']).toBe('# No mermaid diagram content provided');
});
});
describe('fixSubgraphTitleContrast', () => {
it('darkens title text on light subgraph backgrounds (fill attribute)', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
});
it('darkens title text on light subgraph backgrounds (inline style fill)', () => {
const svg = makeSvg(
'<g class="cluster"><rect style="fill: #FFF9C4; stroke: #F9A825"/><g class="cluster-label"><text>Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
});
it('lightens title text on dark subgraph backgrounds', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#1f2020"/><g class="cluster-label"><text fill="#222222">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #f0f0f0');
});
it('leaves title text alone when contrast is already good', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text fill="#333333">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
});
it('skips clusters without a rect', () => {
const svg = makeSvg(
'<g class="cluster"><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
});
it('skips clusters with non-hex fills', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="rgb(255,249,196)"/><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
});
it('sets dark fill when text has no explicit fill on light backgrounds', () => {
const svg = makeSvg(
'<g class="cluster"><rect style="fill:#FFF9C4"/><g class="cluster-label"><text>Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
});
it('preserves existing text style when appending fill override', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text style="font-size: 14px" fill="#E0E0E0">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
const style = svg.querySelector('text')!.getAttribute('style')!;
expect(style).toContain('font-size: 14px');
expect(style).toContain('fill: #1a1a1a');
});
it('handles 3-char hex shorthand fills', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#FFC"/><g class="cluster-label"><text fill="#EEE">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
});
it('avoids double semicolons when existing style has trailing semicolon', () => {
const svg = makeSvg(
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text style="font-size: 14px;" fill="#E0E0E0">Title</text></g></g>',
);
fixSubgraphTitleContrast(svg);
const style = svg.querySelector('text')!.getAttribute('style')!;
expect(style).not.toContain(';;');
expect(style).toContain('fill: #1a1a1a');
});
});
});