Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ <h1 class="text-2xl lg:text-3xl font-bold text-slate-900">{{ vm.name }}</h1>
<div>
<mst-card label="Architecture Details" class="w-full">
<div class="p-6 md:p-8" card-body>
<div class="prose prose-slate max-w-none
<div class="markdown-body prose prose-slate max-w-none
prose-headings:text-slate-900
prose-h2:text-xl prose-h2:font-bold prose-h2:mt-8 prose-h2:mb-4
prose-h3:text-lg prose-h3:font-semibold prose-h3:mt-6 prose-h3:mb-3
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
/* Mermaid diagrams rendered inside [innerHTML] need :host ::ng-deep to pierce encapsulation */
:host ::ng-deep .mermaid-diagram {
/* Diagrams live inside [innerHTML] content, so styling them needs :host ::ng-deep */
:host ::ng-deep .diagram-figure {
position: relative;
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1rem;
background-color: #f8fafc; /* slate-50 */
border: 1px solid #e2e8f0; /* slate-200 */
border-radius: 0.75rem;
overflow-x: auto;
cursor: zoom-in;
transition: border-color 0.15s ease;

&:hover {
border-color: #94a3b8; /* slate-400 */
}

img,
svg {
max-width: 100%;
height: auto;
}
}

.diagram-expand {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
line-height: 1rem;
color: #475569; /* slate-600 */
background-color: rgb(255 255 255 / 0.9);
border: 1px solid #e2e8f0; /* slate-200 */
border-radius: 0.375rem;
/* Kept visible rather than hover-only so the affordance also exists on touch devices */
opacity: 0.75;
transition: opacity 0.15s ease, color 0.15s ease;
}

&:hover .diagram-expand,
.diagram-expand:focus-visible {
opacity: 1;
color: #0f172a; /* slate-900 */
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ReferenceArchitecture, SeoService } from 'app/core';
import { BreadcrumbComponent } from 'app/shared/breadcrumb';
import { BreadcrumbItem } from 'app/shared/breadcrumb/breadcrumb';
import { CardComponent } from 'app/shared/card';
import { DiagramViewerComponent } from 'app/shared/diagram-viewer';
import { Platform, PlatformService } from 'app/shared/platform';
import { ReferenceArchitectureService } from 'app/shared/reference-architecture';
import { TemplateService } from 'app/shared/template';
Expand Down Expand Up @@ -104,14 +105,14 @@ export class ReferenceArchitectureDetailComponent implements OnInit, OnDestroy,
'code.language-mermaid:not([data-mermaid-rendered])'
);

if (mermaidBlocks.length === 0) {
return;
if (mermaidBlocks.length > 0) {
// Mark blocks synchronously before the async render to prevent double-processing
// when ngAfterViewChecked fires again during the async mermaid import.
mermaidBlocks.forEach((block: Element) => block.setAttribute('data-mermaid-rendered', 'true'));
this.renderMermaid(mermaidBlocks);
}

// Mark blocks synchronously before the async render to prevent double-processing
// when ngAfterViewChecked fires again during the async mermaid import.
mermaidBlocks.forEach((block: Element) => block.setAttribute('data-mermaid-rendered', 'true'));
this.renderMermaid(mermaidBlocks);
this.enhanceDiagramImages();
}

public ngOnDestroy(): void {
Expand Down Expand Up @@ -186,14 +187,78 @@ export class ReferenceArchitectureDetailComponent implements OnInit, OnDestroy,

try {
const { svg } = await mermaid.render(id, graphDefinition);
const wrapper = document.createElement('div');
wrapper.classList.add('mermaid-diagram');
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
const holder = document.createElement('div');
holder.innerHTML = svg;
const svgEl = holder.firstElementChild as HTMLElement | null;

if (!svgEl) {
continue;
}

pre.replaceWith(svgEl);
this.makeZoomable(svgEl, svgEl, 'Diagram', null);
} catch {
// Leave the code block as-is if rendering fails
}
}
}

/**
* Diagrams in the markdown body are committed SVGs referenced as images. They are wide and
* detailed, and browser zoom does not help — the page just reflows and the diagram stays fitted
* to its column — so each one gets a fullscreen viewer with real zoom and pan.
*/
private enhanceDiagramImages(): void {
const images: NodeListOf<HTMLImageElement> = this.el.nativeElement.querySelectorAll(
'.markdown-body img:not([data-diagram-enhanced])'
);

images.forEach(img =>
this.makeZoomable(img, img, img.getAttribute('alt') || 'Diagram', img.getAttribute('src'))
);
}

/**
* Wraps `element` in a figure carrying a fullscreen affordance. `viewerNode` is what the viewer
* clones and displays — the same element for images, the bare `<svg>` for rendered mermaid.
*/
private makeZoomable(
element: HTMLElement,
viewerNode: HTMLElement,
title: string,
sourceUrl: string | null
): void {
element.setAttribute('data-diagram-enhanced', 'true');

const figure = document.createElement('figure');
figure.className = 'diagram-figure';

const button = document.createElement('button');
button.type = 'button';
button.className = 'diagram-expand';
button.setAttribute('aria-label', `View ${title} fullscreen`);
button.innerHTML = '<i class="fa-solid fa-expand"></i><span>Fullscreen</span>';

// A markdown image sits alone in its own paragraph — replace that paragraph so the figure does
// not end up nested inside a <p>.
const parent = element.parentElement;
const replaced =
parent?.tagName === 'P' && parent.childElementCount === 1 && !parent.textContent?.trim()
? parent
: element;

replaced.parentElement?.replaceChild(figure, replaced);
figure.appendChild(element);
figure.appendChild(button);

figure.addEventListener('click', () => this.openDiagram(viewerNode, title, sourceUrl));
}

private openDiagram(node: HTMLElement, title: string, sourceUrl: string | null): void {
this.dialog.open(DiagramViewerComponent, {
panelClass: 'diagram-viewer-panel',
data: { title, node, sourceUrl }
});
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<div class="diagram-viewer fixed inset-0 flex flex-col bg-slate-900">
<!-- Toolbar -->
<div class="flex items-center gap-3 px-4 py-3 border-b border-white/10 text-slate-100">
<i class="fa-solid fa-diagram-project text-primary-300"></i>
<span class="text-sm font-semibold truncate flex-1">{{ data.title }}</span>

<div class="flex items-center gap-1 rounded-lg bg-white/10 p-1">
<button
type="button"
class="diagram-viewer-btn"
aria-label="Zoom out"
title="Zoom out (-)"
(click)="zoomOut()">
<i class="fa-solid fa-magnifying-glass-minus"></i>
</button>
<span class="w-14 text-center text-xs font-mono tabular-nums">{{ zoomPercent }}%</span>
<button
type="button"
class="diagram-viewer-btn"
aria-label="Zoom in"
title="Zoom in (+)"
(click)="zoomIn()">
<i class="fa-solid fa-magnifying-glass-plus"></i>
</button>
<button
type="button"
class="diagram-viewer-btn"
aria-label="Fit to screen"
title="Fit to screen (0)"
(click)="resetView()">
<i class="fa-solid fa-compress"></i>
</button>
</div>

<a
*ngIf="data.sourceUrl"
[href]="data.sourceUrl"
target="_blank"
rel="noopener noreferrer"
class="diagram-viewer-btn text-slate-100"
aria-label="Open diagram in a new tab"
title="Open diagram in a new tab">
<i class="fa-solid fa-up-right-from-square"></i>
</a>

<button
type="button"
class="diagram-viewer-btn text-slate-100"
aria-label="Close"
title="Close (Esc)"
(click)="dialogRef.close()">
<i class="fa-solid fa-xmark text-lg"></i>
</button>
</div>

<!-- Stage -->
<div
#stage
class="diagram-viewer-stage flex-1 overflow-hidden"
[class.is-panning]="panning"
(pointerdown)="onPointerDown($event)"
(pointermove)="onPointerMove($event)"
(pointerup)="onPointerUp($event)"
(pointercancel)="onPointerUp($event)"
(dblclick)="onDoubleClick($event)">
<div
#canvas
class="diagram-viewer-canvas"
[style.transform]="'translate(' + translateX + 'px, ' + translateY + 'px)'">
</div>
</div>

<div class="px-4 py-2 text-center text-xs text-slate-400 border-t border-white/10">
Scroll or pinch to zoom · drag to pan · double-click to zoom · Esc to close
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.diagram-viewer-stage {
position: relative;
touch-action: none;
cursor: grab;

&.is-panning {
cursor: grabbing;
}
}

.diagram-viewer-canvas {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
transform-origin: center center;
will-change: transform;
}

.diagram-viewer-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
transition: background-color 0.15s ease;

&:hover {
background-color: rgb(255 255 255 / 0.15);
}
}

/* The diagram is cloned into the canvas at runtime, so it carries no component style attribute. */
:host ::ng-deep .diagram-viewer-content {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
user-select: none;
pointer-events: none;
background-color: #ffffff;
border-radius: 0.5rem;
}
Loading
Loading