From ce210406b9222d49143cd9ce23130ae7276fc7cf Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Mon, 19 Jan 2026 04:24:32 -0600 Subject: [PATCH] feat: Add TimeMachineHistory component with 3D perspective stack New component that displays problem evolution as an Apple Time Machine-style stack of cards: - Current problem is prominent (full opacity, full scale) at the front - Previous problems are stacked behind with perspective depth - Each layer: translateZ, scale down, fade opacity - Clicking past layers triggers navigation to rewind - Smooth animation when new entries are added Co-Authored-By: Claude Opus 4.5 --- apps/web/panda.config.ts | 13 + .../flowchart/TimeMachineHistory.tsx | 266 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 apps/web/src/components/flowchart/TimeMachineHistory.tsx diff --git a/apps/web/panda.config.ts b/apps/web/panda.config.ts index d2859fea..477460a4 100644 --- a/apps/web/panda.config.ts +++ b/apps/web/panda.config.ts @@ -80,6 +80,8 @@ export default defineConfig({ bounceIn: { value: 'bounceIn 1s ease-out' }, // Glow animation (line 6260) glow: { value: 'glow 1s ease-in-out infinite alternate' }, + // Time machine enter animation + timeMachineEnter: { value: 'timeMachineEnter 0.5s ease-out' }, }, }, // Semantic color tokens for light/dark theme support @@ -303,6 +305,17 @@ export default defineConfig({ '60%': { opacity: '1', transform: 'scale(1.1)' }, '100%': { opacity: '1', transform: 'scale(1)' }, }, + // Time machine enter - new history entry slides in from the void + timeMachineEnter: { + '0%': { + opacity: '0', + transform: 'translateZ(-60px) scale(0.85)', + }, + '100%': { + opacity: '1', + transform: 'translateZ(0) scale(1)', + }, + }, }, }, }, diff --git a/apps/web/src/components/flowchart/TimeMachineHistory.tsx b/apps/web/src/components/flowchart/TimeMachineHistory.tsx new file mode 100644 index 00000000..4d769c85 --- /dev/null +++ b/apps/web/src/components/flowchart/TimeMachineHistory.tsx @@ -0,0 +1,266 @@ +'use client' + +import { useRef, useEffect } from 'react' +import type { WorkingProblemHistoryEntry } from '@/lib/flowcharts/schema' +import { css, cx } from '../../../styled-system/css' +import { vstack } from '../../../styled-system/patterns' +import { MathDisplay } from './MathDisplay' + +interface TimeMachineHistoryProps { + /** History of working problem transformations */ + history: WorkingProblemHistoryEntry[] + /** Callback when user clicks a history entry to rewind */ + onNavigate: (index: number) => void + /** Font size for the current (hero) problem */ + fontSize?: 'lg' | 'xl' +} + +/** + * TimeMachineHistory - Apple Time Machine-style 3D perspective stack + * + * Displays the problem evolution as a stack of cards with the current + * problem in front (hero) and past states fading into the background. + * + * - Current problem is prominent (full opacity, full scale) + * - Previous problems are stacked behind with perspective depth + * - Each layer: translateZ, scale down, fade opacity + * - Clicking past layers triggers onNavigate to rewind + */ +export function TimeMachineHistory({ + history, + onNavigate, + fontSize = 'xl', +}: TimeMachineHistoryProps) { + const containerRef = useRef(null) + const latestEntryRef = useRef(null) + + // Scroll to show latest entry when history changes + useEffect(() => { + if (latestEntryRef.current) { + latestEntryRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + }, [history.length]) + + if (history.length === 0) { + return null + } + + // How many history items to show (limit to prevent overwhelming) + const maxVisible = 5 + const visibleHistory = history.slice(-maxVisible) + const hiddenCount = history.length - visibleHistory.length + + return ( +
+ {/* Stack container */} +
+ {/* Hidden count indicator */} + {hiddenCount > 0 && ( +
+ {hiddenCount} earlier step{hiddenCount > 1 ? 's' : ''} hidden +
+ )} + + {/* History entries */} + {visibleHistory.map((entry, visibleIdx) => { + // Calculate actual index in full history + const actualIdx = history.length - visibleHistory.length + visibleIdx + const isLatest = actualIdx === history.length - 1 + // Depth from current: 0 for latest, 1 for previous, etc. + const depth = visibleHistory.length - 1 - visibleIdx + + // 3D transform values based on depth + const translateZ = -depth * 20 // Push back + const scale = 1 - depth * 0.05 // Shrink slightly + const opacity = 1 - depth * 0.15 // Fade + + return ( +
onNavigate(actualIdx) : undefined} + role={!isLatest ? 'button' : undefined} + tabIndex={!isLatest ? 0 : undefined} + onKeyDown={ + !isLatest + ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onNavigate(actualIdx) + } + } + : undefined + } + className={cx( + css({ + width: '100%', + padding: isLatest ? '4 5' : '3 4', + borderRadius: 'xl', + textAlign: 'center', + transformStyle: 'preserve-3d', + transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)', + willChange: 'transform, opacity', + + // Base styling + backgroundColor: isLatest + ? { base: 'blue.50', _dark: 'blue.900' } + : { base: 'gray.50', _dark: 'gray.800' }, + border: isLatest ? '3px solid' : '2px solid', + borderColor: isLatest + ? { base: 'blue.400', _dark: 'blue.500' } + : { base: 'gray.200', _dark: 'gray.700' }, + boxShadow: isLatest ? 'lg' : 'sm', + + // Interactive states for non-latest entries + cursor: isLatest ? 'default' : 'pointer', + _hover: isLatest + ? {} + : { + opacity: '1 !important', + backgroundColor: { base: 'blue.50', _dark: 'blue.900/50' }, + borderColor: { base: 'blue.300', _dark: 'blue.600' }, + transform: 'translateZ(0) scale(1) !important', + }, + _focusVisible: isLatest + ? {} + : { + outline: '2px solid', + outlineColor: { base: 'blue.500', _dark: 'blue.400' }, + outlineOffset: '2px', + }, + }), + // Animation class for new entries + isLatest && + css({ + animation: 'timeMachineEnter 0.5s ease-out', + }) + )} + style={{ + transform: `translateZ(${translateZ}px) scale(${scale})`, + opacity, + zIndex: visibleHistory.length - depth, + }} + > + {/* Math expression */} +
+ +
+ + {/* Step label - shown for all entries */} +
+ {entry.label} +
+ + {/* Step number badge */} +
+ {actualIdx + 1} +
+ + {/* "Current" indicator for latest */} + {isLatest && ( +
+ Now +
+ )} +
+ ) + })} +
+
+ ) +}