Skip to content
Closed
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
69 changes: 69 additions & 0 deletions arbitrum-docs/for-devs/lidia-interactive-diagrams.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Lidia Interactive Diagrams
description: Learn how to use Lidia, a React component for creating interactive, click-to-morph diagrams in your Arbitrum documentation
---

import LidiaDiagram from '@site/src/components/Lidia';
import exampleConfig from '@site/static/diagrams/example/config.json';

# Lidia Interactive Diagrams

Lidia is a React component that enables interactive, click-to-morph diagrams for exploring complex technical concepts. It's particularly useful for visualizing multi-layered architectures, workflows, and system components.

## Example

<LidiaDiagram config={exampleConfig} />

## How it works

- **Click** on highlighted areas to explore deeper levels of detail
- **Navigate back** using the back button to return to previous states
- **Tooltips** appear on hover to guide your exploration

## Using Lidia in your documentation

To add interactive diagrams to your documentation:

1. Create SVG diagrams with clickable areas (elements with unique IDs)
2. Define a configuration file mapping the navigation flow
3. Import and use the `LidiaDiagram` component in your MDX files

### Example usage

```mdx
import LidiaDiagram from '@site/src/components/Lidia';
import myDiagramConfig from '@site/static/diagrams/my-diagram/config.json';

<LidiaDiagram config={myDiagramConfig} />
```

### Configuration structure

```json
{
"id": "diagram-id",
"title": "Diagram Title",
"initialStateId": "overview",
"states": [
{
"id": "overview",
"svgPath": "/diagrams/my-diagram/overview.svg",
"clickableAreas": [
{
"id": "area-1",
"selector": "#svg-element-id",
"nextStateId": "detail-view",
"tooltip": "Click to explore"
}
]
}
]
}
```

## Best practices

- Keep diagrams simple and focused on one concept per state
- Use clear, descriptive IDs for clickable areas
- Provide helpful tooltips to guide users
- Limit navigation depth to 3-5 levels for optimal user experience
5 changes: 5 additions & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const sidebars = {
id: 'for-devs/dev-tools-and-resources/chain-info',
label: 'Chain info',
},
{
type: 'doc',
label: 'Lidia Interactive Diagrams',
id: 'for-devs/lidia-interactive-diagrams',
},
],
},
{
Expand Down
136 changes: 136 additions & 0 deletions website/src/components/Lidia/LidiaDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import type { LidiaDiagramConfig } from './types';
import { useLidiaState } from './hooks/useLidiaState';

interface LidiaDiagramProps {
config: LidiaDiagramConfig;
}

export default function LidiaDiagram({ config }: LidiaDiagramProps) {
const { currentState, navigateToState, goBack, canGoBack } = useLidiaState(config);
const containerRef = useRef<HTMLDivElement>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
const prevStateIdRef = useRef<string | null>(null);
const transitioningRef = useRef(false);

const handleNavigation = useCallback(
(nextStateId: string) => {
if (!transitioningRef.current) {
transitioningRef.current = true;
setIsTransitioning(true);
navigateToState(nextStateId);
}
},
[navigateToState],
);

const handleBackClick = useCallback(() => {
if (!transitioningRef.current) {
transitioningRef.current = true;
setIsTransitioning(true);
goBack();
}
}, [goBack]);

useEffect(() => {
if (!currentState || !containerRef.current) return;

// Skip if we're still showing the same state
if (prevStateIdRef.current === currentState.id) return;

const loadAndSetupSvg = async () => {
try {
const response = await fetch(currentState.svgPath);
const svgText = await response.text();

// Create a new div for the incoming SVG
const newSvgDiv = document.createElement('div');
newSvgDiv.className = 'lidia-svg-wrapper';
newSvgDiv.innerHTML = svgText;

const svg = newSvgDiv.querySelector('svg');
if (!svg) return;

// Set up clickable areas
currentState.clickableAreas.forEach((area) => {
const element = svg.querySelector(area.selector);
if (!element) return;

element.classList.add('lidia-clickable');

if (area.tooltip) {
element.setAttribute('title', area.tooltip);
}

element.addEventListener('click', () => {
handleNavigation(area.nextStateId);
});
});

// Handle transition
const existingWrapper = containerRef.current.querySelector('.lidia-svg-wrapper');

if (existingWrapper && prevStateIdRef.current !== null) {
// Crossfade effect
newSvgDiv.classList.add('lidia-svg-wrapper-entering');
containerRef.current.appendChild(newSvgDiv);

// Force reflow
void newSvgDiv.offsetHeight;

// Start transition
requestAnimationFrame(() => {
existingWrapper.classList.add('lidia-fade-out');
newSvgDiv.classList.remove('lidia-svg-wrapper-entering');
newSvgDiv.classList.add('lidia-fade-in');

// Clean up after transition
setTimeout(() => {
existingWrapper.remove();
transitioningRef.current = false;
setIsTransitioning(false);
}, 600);
});
} else {
// Initial load
if (existingWrapper) existingWrapper.remove();
containerRef.current.appendChild(newSvgDiv);
transitioningRef.current = false;
setIsTransitioning(false);
}

// Update the previous state reference
prevStateIdRef.current = currentState.id;
} catch (error) {
console.error('Error loading SVG:', error);
transitioningRef.current = false;
setIsTransitioning(false);
}
};

loadAndSetupSvg();
}, [currentState, handleNavigation]);

if (!currentState) {
return <div className="lidia-error">Error: Invalid diagram state</div>;
}

return (
<div className="lidia-container">
<div className="lidia-header">
<h3 className="lidia-title">{config.title}</h3>
{canGoBack && (
<button
className="lidia-back-button"
onClick={handleBackClick}
aria-label="Go back to previous state"
disabled={isTransitioning}
>
← Back
</button>
)}
</div>
<div ref={containerRef} className="lidia-svg-container" />
</div>
);
}
39 changes: 39 additions & 0 deletions website/src/components/Lidia/hooks/useLidiaState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState, useCallback } from 'react';
import type { LidiaDiagramConfig, LidiaState } from '../types';

export function useLidiaState(config: LidiaDiagramConfig) {
const [state, setState] = useState<LidiaState>({
currentStateId: config.initialStateId,
history: [config.initialStateId],
});

const navigateToState = useCallback((stateId: string) => {
setState((prev) => ({
currentStateId: stateId,
history: [...prev.history, stateId],
}));
}, []);

const goBack = useCallback(() => {
setState((prev) => {
if (prev.history.length <= 1) return prev;

const newHistory = prev.history.slice(0, -1);
return {
currentStateId: newHistory[newHistory.length - 1],
history: newHistory,
};
});
}, []);

const canGoBack = state.history.length > 1;

const currentState = config.states.find((s) => s.id === state.currentStateId);

return {
currentState,
navigateToState,
goBack,
canGoBack,
};
}
2 changes: 2 additions & 0 deletions website/src/components/Lidia/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './LidiaDiagram';
export type { LidiaDiagramConfig, DiagramState, ClickableArea } from './types';
24 changes: 24 additions & 0 deletions website/src/components/Lidia/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface ClickableArea {
id: string;
selector: string;
nextStateId: string;
tooltip?: string;
}

export interface DiagramState {
id: string;
svgPath: string;
clickableAreas: ClickableArea[];
}

export interface LidiaDiagramConfig {
id: string;
title: string;
states: DiagramState[];
initialStateId: string;
}

export interface LidiaState {
currentStateId: string;
history: string[];
}
3 changes: 3 additions & 0 deletions website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,6 @@
.wrapcode code {
white-space: pre-wrap;
}

/* Import Lidia component styles */
@import './lidia.css';
96 changes: 96 additions & 0 deletions website/src/css/lidia.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.lidia-container {
margin: 2rem 0;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-border-radius);
padding: 1.5rem;
background: var(--ifm-background-surface-color);
}

.lidia-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}

.lidia-title {
margin: 0;
font-size: 1.25rem;
}

.lidia-back-button {
padding: 0.5rem 1rem;
background: var(--ifm-color-primary);
color: white;
border: none;
border-radius: var(--ifm-border-radius);
cursor: pointer;
font-size: 0.875rem;
transition: opacity 0.2s;
}

.lidia-back-button:hover {
opacity: 0.8;
}

.lidia-back-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.lidia-svg-container {
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
position: relative;
}

.lidia-svg-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
transition: opacity 0.6s ease-in-out;
}

.lidia-svg-wrapper svg {
max-width: 100%;
height: auto;
}

/* Crossfade animation classes */
.lidia-svg-wrapper-entering {
opacity: 0;
}

.lidia-svg-wrapper.lidia-fade-out {
opacity: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
}

.lidia-svg-wrapper.lidia-fade-in {
opacity: 1;
}

.lidia-clickable {
cursor: pointer;
transition: opacity 0.2s;
}

.lidia-clickable:hover {
opacity: 0.7;
}

.lidia-error {
padding: 1rem;
background: var(--ifm-color-danger-lightest);
color: var(--ifm-color-danger-dark);
border-radius: var(--ifm-border-radius);
margin: 1rem 0;
}
Loading