Files
vercel__workflow/docs/components/worlds/Globe.tsx
Pranay Prakash 8a146a5bb1 docs: revamp World documentation pages (#763)
* docs: revamp World documentation pages with new structure and design

- Add dedicated world detail pages at /worlds/[id] with MDX-driven content
- Create WorldDetailHero, WorldDetailToc, WorldTestingPerformance components
- Add benchmark history charts and test summary links
- Rewrite local-world, postgres-world, and vercel-world MDX with cleaner structure
- Use h3 headings for configuration options instead of tables/accordions
- Add WorldDataProvider context for passing world data to MDX components
- Add example field to worlds-manifest for linking to example repos
- Update worlds index page design

* fix: update worlds page meta title and description

* feat(docs): improve globe backdrop responsiveness and positioning

- Globe now scales with container width up to 1200px max
- Increased opacity from 20% to 30% for better visibility
- Positioned globe 30% down to show north pole behind header
- Globe component now auto-sizes based on container width

* fix(docs): add light mode support for globe backdrop

* feat: improve world page metadata and add dynamic OG images

- Update title format to '{Name} World | Workflow DevKit'
- Update worlds index page title to 'Worlds | Workflow DevKit'
- Improve world descriptions in worlds-manifest.json for better SEO
- Add dynamic OG image generation at /worlds/[id]/og
- Add openGraph and twitter metadata to world detail pages

* fix: use opengraph-image.tsx file convention for dynamic OG images

- Replace route-based og/route.tsx with opengraph-image.tsx file convention
- Remove runtime = 'edge' to allow generateStaticParams for static generation
- Simplify page.tsx metadata (Next.js auto-detects opengraph-image.tsx)

* docs: improve worlds page UI and restructure deploying section

- Fix globe visibility in light/dark mode
- Fix tooltip positioning on mobile breakpoints
- Remove redundant Documentation button for community worlds
- Add missing pages to Foundations index (streaming, serialization, idempotency)
- Restructure deploying section with new index and building-a-world guide
- Restore original MDX content for official worlds (local, vercel, postgres)
- Auto-expand sidebar folders when child page is active

* docs: simplify worlds page footer sections

* docs: move last updated section under worlds grid

* fix(docs): fix broken links and skip typecheck for interface definitions

- Fix /docs/deploying/world link to /docs/deploying/building-a-world
- Fix /docs/deploying#what-are-worlds invalid anchor to /docs/deploying
- Add @skip-typecheck markers to interface definition code blocks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(docs): address PR review feedback

- benchmark-history/route.ts: Add error handling for JSON parsing,
  distinguish 404s from actual errors
- Globe.tsx: Fix hydration mismatch by deferring theme-dependent
  rendering until component is mounted
- sidebar.tsx: Improve path check robustness for active child detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:16:40 -08:00

127 lines
3.5 KiB
TypeScript

'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import createGlobe, { type COBEOptions } from 'cobe';
import { useTheme } from 'next-themes';
interface GlobeProps {
className?: string;
}
export function Globe({ className }: GlobeProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);
const [size, setSize] = useState(1200);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme();
// Defer theme-dependent rendering until mounted to avoid hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
const updatePointerInteraction = useCallback((value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? 'grabbing' : 'grab';
}
}, []);
const updateMovement = useCallback((clientX: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
pointerInteracting.current = clientX;
}
}, []);
// Track container width
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
setSize(width);
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
useEffect(() => {
if (!canvasRef.current || size === 0 || !mounted) return;
let phi = 0;
const canvasSize = size * 2; // For retina displays
const isDark = resolvedTheme === 'dark';
const globeConfig: COBEOptions = {
devicePixelRatio: 2,
width: canvasSize,
height: canvasSize,
phi: 0,
theta: 0.3,
dark: isDark ? 1 : 0,
diffuse: isDark ? 2 : 1.2,
mapSamples: 16000,
mapBrightness: isDark ? 4 : 2,
baseColor: isDark ? [0.3, 0.3, 0.3] : [1, 1, 1],
markerColor: [0.251, 0.678, 1],
glowColor: isDark ? [0.15, 0.15, 0.15] : [0.95, 0.95, 0.95],
markers: [],
onRender: (state) => {
if (pointerInteracting.current === null) {
phi += 0.003;
}
phi += pointerInteractionMovement.current * 0.01;
pointerInteractionMovement.current *= 0.95;
state.phi = phi;
state.width = canvasSize;
state.height = canvasSize;
},
};
const globe = createGlobe(canvasRef.current, globeConfig);
setTimeout(() => {
if (canvasRef.current) {
canvasRef.current.style.opacity = '1';
}
}, 100);
return () => {
globe.destroy();
};
}, [size, resolvedTheme, mounted]);
return (
<div ref={containerRef} className={className} style={{ aspectRatio: '1' }}>
<canvas
ref={canvasRef}
className="w-full h-full opacity-0 transition-opacity duration-500 cursor-grab"
onPointerDown={(e) => {
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => {
updatePointerInteraction(null);
}}
onPointerOut={() => {
updatePointerInteraction(null);
}}
onMouseMove={(e) => {
updateMovement(e.clientX);
}}
onTouchMove={(e) => {
if (e.touches[0]) {
updateMovement(e.touches[0].clientX);
}
}}
/>
</div>
);
}