Skip to content

Commit 8811d10

Browse files
committed
feat: add GitHub Pages deploy, mobile responsiveness, and shareable URLs
- Configure Vite base path and switch to HashRouter for GitHub Pages - Add deploy workflow triggered on push to main - Update OG/Twitter meta tags with absolute GitHub Pages URLs - Add useScenarioFromUrl hook for shareable scenario links (?scenario=id) - Add mobile-responsive layout with slide-in drawers for sidebars - Hide header text and footer on mobile to maximize screen space - Mock window.matchMedia in test setup for jsdom compatibility
1 parent 26235d5 commit 8811d10

9 files changed

Lines changed: 242 additions & 17 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
pages: write
9+
id-token: write
10+
11+
concurrency:
12+
group: pages
13+
cancel-in-progress: true
14+
15+
jobs:
16+
deploy:
17+
environment:
18+
name: github-pages
19+
url: ${{ steps.deployment.outputs.page_url }}
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
cache: npm
29+
30+
- run: npm ci
31+
32+
- name: Build
33+
run: npm run build
34+
35+
- uses: actions/configure-pages@v5
36+
37+
- uses: actions/upload-pages-artifact@v3
38+
with:
39+
path: dist
40+
41+
- name: Deploy to GitHub Pages
42+
id: deployment
43+
uses: actions/deploy-pages@v4

index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
5+
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Claude Code Algorithm Visualizer</title>
88
<meta
@@ -17,8 +17,11 @@
1717
property="og:description"
1818
content="Interactive visualization of how Claude Code's agentic loop works. Explore flowcharts, step through scenarios, and learn the algorithm behind Claude Code."
1919
/>
20-
<meta property="og:url" content="https://github.com/jonwiggins/claude-code-visualizer" />
21-
<meta property="og:image" content="/og-image.png" />
20+
<meta property="og:url" content="https://jonwiggins.github.io/claude-code-visualizer/" />
21+
<meta
22+
property="og:image"
23+
content="https://jonwiggins.github.io/claude-code-visualizer/og-image.png"
24+
/>
2225

2326
<!-- Twitter Card -->
2427
<meta name="twitter:card" content="summary_large_image" />
@@ -27,7 +30,10 @@
2730
name="twitter:description"
2831
content="Interactive visualization of how Claude Code's agentic loop works. Explore flowcharts, step through scenarios, and learn the algorithm behind Claude Code."
2932
/>
30-
<meta name="twitter:image" content="/og-image.png" />
33+
<meta
34+
name="twitter:image"
35+
content="https://jonwiggins.github.io/claude-code-visualizer/og-image.png"
36+
/>
3137
<link rel="preconnect" href="https://fonts.googleapis.com" />
3238
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
3339
<link

src/App.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
1+
import { HashRouter, Routes, Route, NavLink } from 'react-router-dom';
22
import { VisualizerPage } from './pages/VisualizerPage';
33
import { LearnPage } from './pages/LearnPage';
44
import { Github, Terminal, Play, BookOpen } from 'lucide-react';
55
import clsx from 'clsx';
66

77
function App() {
88
return (
9-
<BrowserRouter>
9+
<HashRouter>
1010
<div className="h-screen flex flex-col bg-[#0d1117] text-[#c9d1d9]">
1111
{/* Header */}
12-
<header className="flex-none h-14 border-b border-[#30363d] flex items-center justify-between px-4">
13-
<div className="flex items-center gap-6">
12+
<header className="flex-none h-14 border-b border-[#30363d] flex items-center justify-between px-2 md:px-4">
13+
<div className="flex items-center gap-3 md:gap-6">
1414
<div className="flex items-center gap-3">
1515
<Terminal size={24} className="text-[#58a6ff]" />
16-
<h1 className="text-lg font-semibold">Claude Code Algorithm</h1>
16+
<h1 className="text-lg font-semibold hidden md:block">Claude Code Algorithm</h1>
1717
</div>
1818

1919
{/* Navigation Tabs */}
@@ -22,29 +22,29 @@ function App() {
2222
to="/learn"
2323
className={({ isActive }) =>
2424
clsx(
25-
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
25+
'flex items-center gap-2 px-3 md:px-4 py-2 rounded-lg text-sm font-medium transition-colors',
2626
isActive
2727
? 'bg-[#1f6feb]/20 text-[#58a6ff]'
2828
: 'text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#1f2428]',
2929
)
3030
}
3131
>
3232
<BookOpen size={16} />
33-
Learn
33+
<span className="hidden md:inline">Learn</span>
3434
</NavLink>
3535
<NavLink
3636
to="/"
3737
className={({ isActive }) =>
3838
clsx(
39-
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
39+
'flex items-center gap-2 px-3 md:px-4 py-2 rounded-lg text-sm font-medium transition-colors',
4040
isActive
4141
? 'bg-[#1f6feb]/20 text-[#58a6ff]'
4242
: 'text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#1f2428]',
4343
)
4444
}
4545
>
4646
<Play size={16} />
47-
Visualizer
47+
<span className="hidden md:inline">Visualizer</span>
4848
</NavLink>
4949
</nav>
5050
</div>
@@ -66,13 +66,13 @@ function App() {
6666
</Routes>
6767

6868
{/* Footer */}
69-
<footer className="flex-none h-8 border-t border-[#30363d] flex items-center justify-center px-4">
69+
<footer className="flex-none h-8 border-t border-[#30363d] hidden md:flex items-center justify-center px-4">
7070
<p className="text-xs text-[#8b949e]">
7171
Educational visualization of Claude Code's agentic loop algorithm
7272
</p>
7373
</footer>
7474
</div>
75-
</BrowserRouter>
75+
</HashRouter>
7676
);
7777
}
7878

src/components/MobileDrawer.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useEffect } from 'react';
2+
import { X } from 'lucide-react';
3+
4+
interface MobileDrawerProps {
5+
open: boolean;
6+
onClose: () => void;
7+
side: 'left' | 'right';
8+
title: string;
9+
children: React.ReactNode;
10+
}
11+
12+
export function MobileDrawer({ open, onClose, side, title, children }: MobileDrawerProps) {
13+
// Prevent body scroll when open
14+
useEffect(() => {
15+
if (open) {
16+
document.body.style.overflow = 'hidden';
17+
} else {
18+
document.body.style.overflow = '';
19+
}
20+
return () => {
21+
document.body.style.overflow = '';
22+
};
23+
}, [open]);
24+
25+
return (
26+
<>
27+
{/* Backdrop */}
28+
{open && (
29+
<div className="fixed inset-0 bg-black/60 z-40" onClick={onClose} aria-hidden="true" />
30+
)}
31+
32+
{/* Drawer */}
33+
<div
34+
className={`fixed top-0 ${side === 'left' ? 'left-0' : 'right-0'} h-full w-[85vw] max-w-[380px] bg-[#0d1117] border-${side === 'left' ? 'r' : 'l'} border-[#30363d] z-50 flex flex-col transition-transform duration-300 ease-in-out ${
35+
open ? 'translate-x-0' : side === 'left' ? '-translate-x-full' : 'translate-x-full'
36+
}`}
37+
>
38+
{/* Header */}
39+
<div className="flex-none h-12 border-b border-[#30363d] flex items-center justify-between px-4">
40+
<span className="text-sm font-semibold text-[#c9d1d9]">{title}</span>
41+
<button
42+
onClick={onClose}
43+
className="p-1 hover:bg-[#30363d] rounded transition-colors text-[#8b949e] hover:text-[#c9d1d9]"
44+
>
45+
<X size={18} />
46+
</button>
47+
</div>
48+
49+
{/* Content */}
50+
<div className="flex-1 overflow-auto p-4 space-y-4">{children}</div>
51+
</div>
52+
</>
53+
);
54+
}

src/hooks/useMediaQuery.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useState, useEffect } from 'react';
2+
3+
export function useMediaQuery(query: string): boolean {
4+
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
5+
6+
useEffect(() => {
7+
const mql = window.matchMedia(query);
8+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
9+
mql.addEventListener('change', handler);
10+
return () => mql.removeEventListener('change', handler);
11+
}, [query]);
12+
13+
return matches;
14+
}
15+
16+
export function useIsMobile(): boolean {
17+
return useMediaQuery('(max-width: 767px)');
18+
}

src/hooks/useScenarioFromUrl.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect } from 'react';
2+
import { useSearchParams } from 'react-router-dom';
3+
import { useVisualizerStore } from '../store';
4+
5+
export function useScenarioFromUrl() {
6+
const [searchParams, setSearchParams] = useSearchParams();
7+
const currentScenario = useVisualizerStore((s) => s.currentScenario);
8+
const selectScenario = useVisualizerStore((s) => s.selectScenario);
9+
const scenarios = useVisualizerStore((s) => s.scenarios);
10+
11+
// On mount: read scenario from URL
12+
useEffect(() => {
13+
const scenarioId = searchParams.get('scenario');
14+
if (scenarioId && scenarios.some((s) => s.id === scenarioId)) {
15+
selectScenario(scenarioId);
16+
}
17+
// Only run on mount
18+
// eslint-disable-next-line react-hooks/exhaustive-deps
19+
}, []);
20+
21+
// On scenario change: update URL
22+
useEffect(() => {
23+
if (currentScenario) {
24+
setSearchParams({ scenario: currentScenario.id }, { replace: true });
25+
} else {
26+
setSearchParams({}, { replace: true });
27+
}
28+
}, [currentScenario, setSearchParams]);
29+
}

src/pages/VisualizerPage.tsx

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ import { SandboxPanel } from '../components/SandboxPanel';
55
import { PlaybackControls } from '../components/PlaybackControls';
66
import { ExecutionLog } from '../components/ExecutionLog';
77
import { DetailPanel } from '../components/DetailPanel';
8-
import { GripVertical } from 'lucide-react';
8+
import { MobileDrawer } from '../components/MobileDrawer';
9+
import { GripVertical, List, SlidersHorizontal } from 'lucide-react';
10+
import { useIsMobile } from '../hooks/useMediaQuery';
11+
import { useScenarioFromUrl } from '../hooks/useScenarioFromUrl';
912

1013
export function VisualizerPage() {
11-
// Sidebar resizing state
14+
useScenarioFromUrl();
15+
const isMobile = useIsMobile();
16+
17+
// Mobile drawer state
18+
const [leftDrawerOpen, setLeftDrawerOpen] = useState(false);
19+
const [rightDrawerOpen, setRightDrawerOpen] = useState(false);
20+
21+
// Sidebar resizing state (desktop only)
1222
const [leftSidebarWidth, setLeftSidebarWidth] = useState(340);
1323
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
1424
const [resizingSide, setResizingSide] = useState<'left' | 'right' | null>(null);
@@ -56,6 +66,55 @@ export function VisualizerPage() {
5666
};
5767
}, [resizingSide, resize, stopResizing]);
5868

69+
if (isMobile) {
70+
return (
71+
<div className="flex-1 relative min-h-0">
72+
{/* Full-width canvas */}
73+
<div className="h-full p-2">
74+
<FlowCanvas />
75+
</div>
76+
77+
{/* Floating buttons */}
78+
<button
79+
onClick={() => setLeftDrawerOpen(true)}
80+
className="absolute bottom-4 left-4 z-30 p-3 bg-[#1f6feb] hover:bg-[#388bfd] text-white rounded-full shadow-lg transition-colors"
81+
aria-label="Open scenarios"
82+
>
83+
<List size={20} />
84+
</button>
85+
<button
86+
onClick={() => setRightDrawerOpen(true)}
87+
className="absolute bottom-4 right-4 z-30 p-3 bg-[#1f6feb] hover:bg-[#388bfd] text-white rounded-full shadow-lg transition-colors"
88+
aria-label="Open controls"
89+
>
90+
<SlidersHorizontal size={20} />
91+
</button>
92+
93+
{/* Drawers */}
94+
<MobileDrawer
95+
open={leftDrawerOpen}
96+
onClose={() => setLeftDrawerOpen(false)}
97+
side="left"
98+
title="Scenarios"
99+
>
100+
<ScenarioPicker />
101+
<SandboxPanel />
102+
</MobileDrawer>
103+
104+
<MobileDrawer
105+
open={rightDrawerOpen}
106+
onClose={() => setRightDrawerOpen(false)}
107+
side="right"
108+
title="Controls"
109+
>
110+
<PlaybackControls />
111+
<ExecutionLog />
112+
<DetailPanel />
113+
</MobileDrawer>
114+
</div>
115+
);
116+
}
117+
59118
return (
60119
<div className="flex-1 flex min-h-0">
61120
{/* Left sidebar - Scenarios & Sandbox (resizable) */}

src/test/setup.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
import '@testing-library/jest-dom/vitest';
2+
3+
// Mock window.matchMedia for jsdom
4+
Object.defineProperty(window, 'matchMedia', {
5+
writable: true,
6+
value: (query: string) => ({
7+
matches: false,
8+
media: query,
9+
onchange: null,
10+
addListener: () => {},
11+
removeListener: () => {},
12+
addEventListener: () => {},
13+
removeEventListener: () => {},
14+
dispatchEvent: () => false,
15+
}),
16+
});

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react';
44
import tailwindcss from '@tailwindcss/vite';
55

66
export default defineConfig({
7+
base: '/claude-code-visualizer/',
78
plugins: [react(), tailwindcss()],
89
test: {
910
globals: true,

0 commit comments

Comments
 (0)