diff --git a/Ralph/IMPLEMENTATION_PLAN.md b/Ralph/IMPLEMENTATION_PLAN.md index 4f46bde..677b96b 100644 --- a/Ralph/IMPLEMENTATION_PLAN.md +++ b/Ralph/IMPLEMENTATION_PLAN.md @@ -79,11 +79,11 @@ src/ Run `npm create vite@latest . -- --template react-ts` to scaffold the project. Install dependencies: `npm install framer-motion lucide-react`. Initialize Tailwind: `npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p`. Configure `tailwind.config.js` with custom colors (teal #00897B, coral #FF6B6B, etc.). Set up `src/index.css` with Tailwind directives and CSS custom properties matching concept.html. -- [ ] **Task 2: Set up project structure and types** +- [x] **Task 2: Set up project structure and types** Create the folder structure (`components/`, `hooks/`, `lib/`, `types/`). Define TypeScript interfaces in `types/index.ts` for: `Skill` (name, level, category, color), `Experience` (role, org, date, bullets), `Education` (degree, institution, period, detail), `Project` (title, description, link?). Create `lib/utils.ts` with helper function `calculateSkillOffset(level: number, radius: number): number` that returns `2 * Math.PI * radius * (1 - level / 100)`. -- [ ] **Task 3: Build BootSequence component** +- [x] **Task 3: Build BootSequence component** Create `components/BootSequence.tsx`. Implement terminal typing animation using Framer Motion or CSS transitions. Display boot lines with correct colors (cyan labels, green values, dim text). Use exact boot text from concept.html: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION, module loading, [OK] lines, READY. Duration: ~4 seconds. Emit `onComplete` callback when finished. Styling: black background, Fira Code font. diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 72aa7b3..e2f2237 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -46,3 +46,21 @@ - **Learnings**: - Need `src/vite-env.d.ts` with `/// ` for CSS imports - Vite refuses to scaffold in non-empty directory, so manual setup was needed + +### Iteration 2 — Task 2 & 3: Project structure and BootSequence +- **Completed**: Task 2 (Set up project structure and types) - was already done in Task 1 +- **Completed**: Task 3 - Build BootSequence component +- **Files created**: + - `src/components/BootSequence.tsx` - Terminal typing animation using Framer Motion +- **Design decisions**: + - Used Framer Motion's `motion.div` with `initial`/`animate` props for line reveals + - Each line animates with opacity 0→1, translateY 8px→0 over 400ms + - Staggered delays calculated from cumulative 220ms per line + - Blinking cursor implemented with CSS animation class `animate-blink` + - Used `AnimatePresence` for smooth exit fade (800ms) +- **Boot sequence timing preserved**: 14 lines × 220ms + 400ms pause + 800ms fade = ~4.28s +- **Quality checks**: `npm run typecheck` ✓, `npm run build` ✓, `npm run lint` ✓ +- **Learnings**: + - Framer Motion's delay prop uses seconds, not milliseconds + - Used `dangerouslySetInnerHTML` for colored spans within boot lines (matches concept.html structure) + - CSS classes for blink/seed-pulse animations already existed in index.css from Task 1 diff --git a/src/App.tsx b/src/App.tsx index 601f54c..7bbeb84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import type { Phase } from './types' +import { BootSequence } from './components/BootSequence' function App() { const [phase, setPhase] = useState('boot') @@ -7,13 +8,16 @@ function App() { return (
{phase === 'boot' && ( -
-
CLINICAL TERMINAL v3.2.1
+ setPhase('ecg')} /> + )} + + {phase === 'ecg' && ( +
)} diff --git a/src/components/BootSequence.tsx b/src/components/BootSequence.tsx new file mode 100644 index 0000000..e415384 --- /dev/null +++ b/src/components/BootSequence.tsx @@ -0,0 +1,95 @@ +import { motion, AnimatePresence } from 'framer-motion' +import { useEffect, useState } from 'react' + +interface BootLine { + html: string + delay: number +} + +const bootLines: BootLine[] = [ + { html: 'CLINICAL TERMINAL v3.2.1', delay: 0 }, + { html: 'Initialising pharmacist profile...', delay: 220 }, + { html: '---', delay: 220 }, + { html: 'SYSTEM NHS Norfolk & Waveney ICB', delay: 220 }, + { html: 'USER Andy Charlwood', delay: 220 }, + { html: 'ROLE Deputy Head of Population Health & Data Analysis', delay: 220 }, + { html: 'LOCATION Norwich, UK', delay: 220 }, + { html: '---', delay: 220 }, + { html: 'Loading modules...', delay: 220 }, + { html: '[OK] pharmacist_core.sys', delay: 220 }, + { html: '[OK] population_health.mod', delay: 220 }, + { html: '[OK] data_analytics.eng', delay: 220 }, + { html: '---', delay: 220 }, + { html: '> READY — Rendering CV...', delay: 220 }, +] + +interface BootSequenceProps { + onComplete: () => void +} + +export function BootSequence({ onComplete }: BootSequenceProps) { + const [isVisible, setIsVisible] = useState(true) + const [lineDelays, setLineDelays] = useState([]) + + useEffect(() => { + const delays: number[] = [] + let totalDelay = 0 + bootLines.forEach((line) => { + delays.push(totalDelay) + totalDelay += line.delay + }) + setLineDelays(delays) + + const totalBootTime = totalDelay + const fadeStartTime = totalBootTime + 400 + + const fadeTimer = setTimeout(() => { + setIsVisible(false) + }, fadeStartTime) + + const completeTimer = setTimeout(() => { + onComplete() + }, fadeStartTime + 800) + + return () => { + clearTimeout(fadeTimer) + clearTimeout(completeTimer) + } + }, [onComplete]) + + return ( + + {isVisible && ( + +
+ {bootLines.map((line, index) => ( + + ))} + +
+
+ )} +
+ ) +}