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
1,362 changes: 1,283 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "portfolio",
"private": true,
"homepage": "https://cagesthrottleus.github.io/",
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -26,6 +26,7 @@
"@spectrum-icons/workflow": "^4.2.25",
"framer-motion": "^12.23.24",
"lucide-react": "^0.555.0",
"mermaid": "^11.12.1",
"react": "^19.2.0",
"react-aria-components": "^1.13.0",
"react-dom": "^19.2.0",
Expand Down Expand Up @@ -54,7 +55,7 @@
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"jsdom": "^27.2.0",
"prettier": "^3.7.2",
"prettier": "^3.7.3",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "npm:rolldown-vite@7.2.8",
Expand Down
173 changes: 173 additions & 0 deletions src/components/BlogPost/BlogComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
* blog authors to know CSS class names.
*/

import { useEffect, useRef, useState } from "react";

import { MERMAID_CONFIG } from "../../utils/constants";

import type { ReactNode } from "react";

interface RedactedProps {
Expand Down Expand Up @@ -92,6 +96,173 @@ export function Highlight({ children }: HighlightProps) {
return <mark className="blog-highlight">{children}</mark>;
}

interface TableProps {
headers: string[];
rows: Array<Array<ReactNode>>;
}

/**
* Table component with classified document styling
* Supports JSX elements in cells (unlike markdown tables)
*
* Usage:
* <Table
* headers={['Metric', 'Value', 'Status']}
* rows={[
* ['Initial Load Time', <Highlight>&lt;2 seconds</Highlight>, '✓ Optimal'],
* ['Cache Hit Rate', <Highlight>87%</Highlight>, '✓ Optimal']
* ]}
* />
*/
export function Table({ headers, rows }: TableProps) {
return (
<table className="blog-table">
<thead>
<tr>
{headers.map((header, i) => (
<th key={i}>{header}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}

interface MermaidProps {
children: string;
caption?: string;
}

// Track mermaid initialization globally
let mermaidInitialized = false;

/**
* Mermaid diagram component with classified document styling
* Renders diagrams using Cold War intelligence color palette
* Uses safe ref-based rendering (no dangerouslySetInnerHTML)
* Lazy loads Mermaid library only when component is used
*
* Usage:
* <Mermaid caption="Fig 1.1: System Architecture">
* {`
* graph TD
* A[Client] --> B[Server]
* B --> C[Database]
* `}
* </Mermaid>
*/
export function Mermaid({ children, caption }: MermaidProps) {
const diagramRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string>("");
const [isRendered, setIsRendered] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const currentRef = diagramRef.current;
let cancelled = false;

const loadAndRenderDiagram = async () => {
if (!currentRef) return;

try {
setIsLoading(true);

// Lazy load mermaid library (only when Mermaid component is used)
const mermaidModule = await import("mermaid");
const mermaid = mermaidModule.default;

// Check if cancelled after async import
if (cancelled) return;

// Initialize mermaid once with Cold War classified theme
if (!mermaidInitialized) {
mermaid.initialize(MERMAID_CONFIG);
mermaidInitialized = true;
}

// Clear previous content safely (use innerHTML to avoid DOM removal errors)
currentRef.innerHTML = "";
currentRef.removeAttribute("data-processed");

// Create a temporary div for mermaid to process
const tempDiv = document.createElement("div");
tempDiv.className = "mermaid";
tempDiv.textContent = children.trim();
currentRef.appendChild(tempDiv);

// Render the diagram directly into the DOM (safe approach)
await mermaid.run({
nodes: [tempDiv],
});

// Prevent state updates after unmount (cleanup can set cancelled during async)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (cancelled) return;
setError("");
setIsRendered(true);
} catch (err) {
if (cancelled) return;
setError(
err instanceof Error ? err.message : "Failed to render diagram",
);
setIsRendered(false);
} finally {
if (cancelled) return;
setIsLoading(false);
}
};

void loadAndRenderDiagram();

return () => {
cancelled = true;
};
}, [children]);

if (error) {
return (
<div className="blog-mermaid-error">
<CallOut type="error">
<strong>Diagram Rendering Error:</strong>
<pre>{error}</pre>
</CallOut>
</div>
);
}

return (
<figure className="blog-mermaid">
<div
ref={diagramRef}
className="blog-mermaid-diagram"
style={{
opacity: isRendered ? 1 : 0,
transition: "opacity 0.3s",
minHeight: isLoading ? "200px" : undefined,
}}
>
{isLoading && (
<div
style={{ textAlign: "center", padding: "3rem", color: "#22c55e" }}
>
Loading diagram renderer...
</div>
)}
</div>
{caption && <figcaption className="blog-caption">{caption}</figcaption>}
</figure>
);
}

/**
* Collection of all blog components
* Pass this to MDXContent as the components prop
Expand All @@ -103,4 +274,6 @@ export const blogComponents = {
SectionMarker,
ImageWithCaption,
Highlight,
Table,
Mermaid,
};
92 changes: 86 additions & 6 deletions src/components/BlogPost/BlogPostLayout.css
Original file line number Diff line number Diff line change
Expand Up @@ -232,29 +232,34 @@
}

/* Tables */
.blog-post-content table {
.blog-post-content table,
.blog-table {
width: 100%;
border-collapse: collapse;
margin: var(--spacing-lg) 0;
font-size: var(--font-size-sm);
}

.blog-post-content th,
.blog-post-content td {
.blog-post-content td,
.blog-table th,
.blog-table td {
border: var(--border-width-thin) solid var(--color-border-primary);
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
}

.blog-post-content th {
.blog-post-content th,
.blog-table th {
background: rgba(220, 38, 38, 0.1);
color: var(--color-text-primary);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
}

.blog-post-content tr:nth-child(even) {
.blog-post-content tr:nth-child(even),
.blog-table tbody tr:nth-child(even) {
background: rgba(10, 10, 10, 0.3);
}

Expand Down Expand Up @@ -284,8 +289,8 @@
}

.blog-callout-info {
border-color: var(--terminal-500);
background: rgba(34, 197, 94, 0.05);
border-color: var(--steel-500);
background: rgba(59, 130, 246, 0.05);
}

.blog-callout-warning {
Expand Down Expand Up @@ -363,6 +368,81 @@
font-size: var(--font-size-xl);
}

/* Mermaid Diagrams */
.blog-mermaid {
margin: var(--spacing-xl) 0;
padding: var(--spacing-lg);
background: rgba(10, 10, 10, 0.6);
border: var(--border-width-normal) solid var(--color-border-primary);
border-radius: var(--radius-sm);
box-shadow:
0 0 20px rgba(220, 38, 38, 0.1),
inset 0 0 40px rgba(220, 38, 38, 0.03);
}

.blog-mermaid-diagram {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
padding: var(--spacing-md);
background: rgba(0, 0, 0, 0.3);
border-radius: var(--radius-xs);
overflow-x: auto;
}

/* Ensure mermaid SVGs are properly styled */
.blog-mermaid-diagram svg {
max-width: 100%;
height: auto;
filter: drop-shadow(0 0 10px rgba(220, 38, 38, 0.2));
}

/* Override mermaid's default styles with Cold War theme */
.blog-mermaid-diagram .node rect,
.blog-mermaid-diagram .node circle,
.blog-mermaid-diagram .node ellipse,
.blog-mermaid-diagram .node polygon {
stroke: var(--classified-500) !important;
stroke-width: 2px !important;
}

.blog-mermaid-diagram .edgePath .path {
stroke: var(--terminal-500) !important;
stroke-width: 2px !important;
}

.blog-mermaid-diagram .arrowheadPath {
fill: var(--terminal-500) !important;
stroke: var(--terminal-500) !important;
}

.blog-mermaid-diagram .cluster rect {
stroke: var(--terminal-500) !important;
stroke-width: 2px !important;
}

/* Mermaid text elements */
.blog-mermaid-diagram text {
font-family: var(--font-mono) !important;
fill: var(--color-text-primary) !important;
}

.blog-mermaid-diagram .edgeLabel text {
fill: var(--color-text-secondary) !important;
}

/* Mermaid error state */
.blog-mermaid-error {
margin: var(--spacing-lg) 0;
}

.blog-mermaid-error pre {
margin-top: var(--spacing-sm);
font-size: var(--font-size-xs);
color: var(--color-text-muted);
}

/* ============================================
* RESPONSIVE
* ============================================ */
Expand Down
Loading