This commit is contained in:
2026-04-24 13:46:52 +02:00
parent a3ccba0663
commit 7b115c2284
5 changed files with 0 additions and 1097 deletions

View File

@@ -1,378 +0,0 @@
# Experience Timeline Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the horizontal experience card carousel with a full-width vertical timeline that surfaces job descriptions and sub-phase breakdowns from static data.
**Architecture:** Extend the `Job` type with optional `summary` and `sub_phases` fields, update `data.ts` with CV content, then replace the card carousel with a new `ExperienceTimeline` server component. No client-side JS — all content is statically rendered.
**Tech Stack:** Next.js (App Router), TypeScript, TailwindCSS
---
## File Map
| Action | File | Responsibility |
|--------|------|---------------|
| Modify | `lib/types.ts` | Add `JobSubPhase`, extend `Job` |
| Modify | `lib/data.ts` | Add `summary` + `sub_phases` to WPP and GIAP entries |
| Create | `components/experience-timeline.tsx` | Renders the full vertical timeline (server component) |
| Modify | `components/experience.tsx` | Swap card loop for `<ExperienceTimeline>` |
| Delete | `components/experience-card.tsx` | Replaced by timeline component |
---
### Task 1: Extend types
**Files:**
- Modify: `lib/types.ts`
- [ ] **Step 1: Add `JobSubPhase` and extend `Job`**
Replace the entire contents of `lib/types.ts` with:
```typescript
export interface Skill {
name: string;
}
export interface JobSubPhase {
label: string;
start_date: string;
end_date: string | null;
bullets: string[];
}
export interface Job {
id: number;
position: string;
company: string;
still_working: boolean;
start_date: string;
end_date: string | null;
technologies: string[];
summary?: string;
sub_phases?: JobSubPhase[];
}
export interface Project {
id: number;
name: string;
short_description: string;
description: string;
technologies: string[];
thumbnails: string[];
category: 'Web' | 'Mobile' | 'Desktop' | 'Api' | 'Game';
github_url?: string | null;
visit_url?: string | null;
download_url?: string | null;
commercial?: boolean;
}
```
- [ ] **Step 2: Verify TypeScript is happy**
```bash
npx tsc --noEmit
```
Expected: no errors (existing job entries have no `sub_phases` yet, so optional fields are fine).
- [ ] **Step 3: Commit**
```bash
git add lib/types.ts
git commit -m "feat: add JobSubPhase type and extend Job with summary and sub_phases"
```
---
### Task 2: Update job data
**Files:**
- Modify: `lib/data.ts`
- [ ] **Step 1: Update the WPP entry (id: 8)**
Replace the WPP job object in the `jobs` array:
```typescript
{
id: 8,
position: "Software Engineer",
company: "WPP Media | Choreograph | Wavemaker",
still_working: true,
start_date: "2023-09-13",
end_date: null,
summary:
"Advanced from frontend UI development to backend systems engineering, leading infrastructure-agnostic design initiatives.",
sub_phases: [
{
label: "Backend & Infrastructure",
start_date: "2025-03-01",
end_date: null,
bullets: [
"Engineered and optimized backend applications and internal tools using Python, FastAPI, and Django.",
"Streamlined CI/CD workflows and containerized applications with GitLab Pipelines, Docker, and Kubernetes across GCP and Azure environments.",
],
},
{
label: "Frontend Architecture",
start_date: "2023-09-13",
end_date: "2025-03-01",
bullets: [
"Architected scalable microfrontends utilizing Angular and Module Federation, seamlessly integrating standalone internal tools into a unified enterprise shell application.",
"Ensured seamless integration of UI components with Kubernetes-based deployments and Azure Pipelines.",
],
},
],
technologies: [
"Angular",
"Azure",
"Azure Pipelines",
"Django",
"Docker",
"FastAPI",
"GCP",
"Gitlab CI",
"Gitlab Pipelines",
"Kubernetes",
"PostgreSQL",
"Python",
"React",
"SCSS",
"TailwindCSS",
"Typescript",
],
},
```
- [ ] **Step 2: Update the GIAP entry (id: 2)**
Replace the GIAP job object:
```typescript
{
id: 2,
position: "Full Stack Developer",
company: "GIAP",
still_working: false,
start_date: "2021-05-19",
end_date: "2023-02-03",
sub_phases: [
{
label: "Desktop / Backend",
start_date: "2021-05-19",
end_date: "2022-02-01",
bullets: [
"Architected and optimized complex PostGIS/PostgreSQL cross-database comparison queries utilizing Common Table Expressions (CTEs), drastically reducing execution time from over 5 minutes to under 15 seconds.",
"Developed a robust GIS data assertion module using Python and Qt to automatically validate spatial data against strict compliance standards.",
],
},
{
label: "Frontend",
start_date: "2022-02-01",
end_date: "2023-02-03",
bullets: [
"Engineered a comprehensive, public-facing web application for the City of Gdańsk (geogdansk.pl) leveraging React, TypeScript, Redux, and the ArcGIS JS API.",
],
},
],
technologies: [
"Python",
"React",
"Typescript",
"PostgreSQL",
"PostGIS",
"ArcGIS JS API",
"Redux",
"Qt",
"QGIS",
"Git",
],
},
```
- [ ] **Step 3: Verify TypeScript is happy**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 4: Commit**
```bash
git add lib/data.ts
git commit -m "feat: add sub_phases and summary to WPP and GIAP job entries"
```
---
### Task 3: Create ExperienceTimeline component
**Files:**
- Create: `components/experience-timeline.tsx`
- [ ] **Step 1: Create the component**
```tsx
import { Job } from "@/lib/types";
import Chip from "./chip";
import formatDate from "@/utils/format-date";
const ExperienceTimeline = ({ jobs }: { jobs: Job[] }) => (
<div className="relative w-full max-w-3xl mx-auto flex flex-col gap-0">
<div className="absolute left-[9px] top-3 bottom-3 w-px bg-gradient-to-b from-white/30 to-white/0" />
{jobs.map((job) => (
<div key={job.id} className="flex gap-6 pb-10 last:pb-0">
<div className="flex flex-col items-center shrink-0 w-5 pt-1.5 z-10">
<div className="w-3 h-3 rounded-full bg-white/40 border border-white/20 shadow-[0_0_8px_rgba(255,255,255,0.2)]" />
</div>
<div className="flex-1 bg-white/5 border border-white/10 rounded-2xl p-5 hover:border-white/20 transition-colors">
<div className="flex justify-between items-start flex-wrap gap-1 mb-1">
<h4 className="text-lg font-semibold text-white">{job.position}</h4>
<span className="text-xs text-white/40">
{formatDate(job.start_date)} {" "}
{job.still_working ? "Present" : formatDate(job.end_date!)}
</span>
</div>
<p className="text-sm text-white/60 mb-4">{job.company}</p>
{job.summary && (
<p className="text-sm text-white/50 italic mb-4">{job.summary}</p>
)}
{job.sub_phases && job.sub_phases.length > 0 && (
<div className="flex flex-col gap-3 mb-4">
{job.sub_phases.map((phase) => (
<div
key={phase.label}
className="bg-white/[0.03] border border-white/[0.07] rounded-xl px-4 py-3"
>
<div className="flex justify-between items-center flex-wrap gap-1 mb-2">
<span className="text-[11px] font-semibold uppercase tracking-widest text-white/50">
{phase.label}
</span>
<span className="text-[10px] text-white/30">
{formatDate(phase.start_date)} {" "}
{phase.end_date ? formatDate(phase.end_date) : "Present"}
</span>
</div>
<ul className="list-disc list-inside space-y-1">
{phase.bullets.map((b) => (
<li key={b} className="text-xs text-white/50 leading-relaxed">
{b}
</li>
))}
</ul>
</div>
))}
</div>
)}
<div className="border-t border-white/10 pt-4 mt-2">
<p className="text-[10px] uppercase tracking-widest text-white/30 mb-2">
Technologies
</p>
<div className="flex flex-wrap gap-2">
{job.technologies.map((tech) => (
<Chip key={tech} text={tech} />
))}
</div>
</div>
</div>
</div>
))}
</div>
);
export default ExperienceTimeline;
```
- [ ] **Step 2: Verify TypeScript is happy**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add components/experience-timeline.tsx
git commit -m "feat: add ExperienceTimeline server component"
```
---
### Task 4: Wire up timeline in Experience section and remove old card
**Files:**
- Modify: `components/experience.tsx`
- Delete: `components/experience-card.tsx`
- [ ] **Step 1: Replace `experience.tsx`**
```tsx
import { Job } from "@/lib/types";
import ExperienceTimeline from "@/components/experience-timeline";
const Experience = ({ jobs }: { jobs: Job[] }) => (
<div
id="experience"
className="flex flex-col items-center gap-8 p-4 w-full"
>
<h3 className="mt-4 text-5xl font-bold tracking-tight text-white">
Experience
</h3>
<ExperienceTimeline jobs={jobs} />
</div>
);
export default Experience;
```
- [ ] **Step 2: Delete the old card component**
```bash
rm components/experience-card.tsx
```
- [ ] **Step 3: Verify no remaining imports of ExperienceCard**
```bash
grep -r "experience-card\|ExperienceCard" /mnt/drive/dev/gabrielkaszewski-next --include="*.tsx" --include="*.ts" | grep -v node_modules
```
Expected: no output.
- [ ] **Step 4: Verify TypeScript is happy**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 5: Start dev server and visually verify the timeline renders correctly**
```bash
npm run dev
```
Open `http://localhost:3000` and scroll to the Experience section. Verify:
- Vertical line and dots are visible
- WPP shows summary + two sub-phase blocks with bullets
- digimonkeys.com shows only dates + chips (no sub-phases)
- GIAP shows two sub-phase blocks with bullets
- Tech chips render under each entry
- [ ] **Step 6: Commit**
```bash
git add components/experience.tsx
git commit -m "feat: replace experience card carousel with vertical timeline"
```

View File

@@ -1,247 +0,0 @@
# Gravity Engine Bug Fixes Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix three root-cause bugs in `GravityEngine`: too many physics bodies (lag), Phase 1 layout thrashing (startup freeze), and inline style destruction on restore (image breakage).
**Architecture:** All three fixes are in `lib/gravity-engine.ts`. The `dirtyElements` field changes from `HTMLElement[]` to `Array<{el, originalCssText}>` so `stop()` restores exact pre-gravity inline styles (fixing image breakage). `start()` is rewritten with a clean read-then-write two-pass approach and a `MAX_DEPTH = 3` depth cap so sections/cards become physics units instead of hundreds of atomic text nodes (fixing lag and startup freeze). No other files change.
**Tech Stack:** TypeScript, browser DOM APIs
---
## Files
- **Modify:** `lib/gravity-engine.ts` — only file changed
---
### Task 1: Rewrite `start()` and `stop()` with all three fixes
**Files:**
- Modify: `lib/gravity-engine.ts`
- [ ] **Step 1: Read the current file**
```bash
cat /mnt/drive/dev/gabrielkaszewski-next/lib/gravity-engine.ts
```
Confirm it starts with the `PhysicsBody` interface and `GravityEngine` class with `bodies`, `dirtyElements`, `animationFrameId`, `isRunning`, and `INITIAL_FORCE` fields.
- [ ] **Step 2: Replace the `dirtyElements` field**
Find this line in `GravityEngine`:
```typescript
private dirtyElements: HTMLElement[] = [];
```
Replace with:
```typescript
private dirtyElements: Array<{ el: HTMLElement; originalCssText: string }> = [];
```
- [ ] **Step 3: Replace the entire `start()` method**
Find and replace the full `start()` method with:
```typescript
start() {
if (this.isRunning) return;
this.isRunning = true;
this.dirtyElements = [];
const MAX_DEPTH = 3;
const containers = document.querySelectorAll(".gravity-body");
const leafElements: HTMLElement[] = [];
const intermediates: { el: HTMLElement; w: number; h: number }[] = [];
// READ-ONLY traversal: classify all elements and snapshot sizes.
// No DOM writes here — one layout flush for the entire traversal.
const collectElements = (el: HTMLElement, depth: number) => {
// Skip purely decorative elements (e.g. background images).
if (el.classList.contains("pointer-events-none")) return;
const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes(
el.tagName.toUpperCase(),
);
// Treat as a leaf if solid, childless, or at the depth cap.
if (isSolid || el.children.length === 0 || depth >= MAX_DEPTH) {
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
leafElements.push(el);
}
return;
}
intermediates.push({ el, w: el.offsetWidth, h: el.offsetHeight });
Array.from(el.children).forEach((child) =>
collectElements(child as HTMLElement, depth + 1),
);
};
const rootSizes: { el: HTMLElement; w: number; h: number }[] = [];
containers.forEach((container) => {
const htmlContainer = container as HTMLElement;
rootSizes.push({
el: htmlContainer,
w: htmlContainer.offsetWidth,
h: htmlContainer.offsetHeight,
});
Array.from(htmlContainer.children).forEach((child) =>
collectElements(child as HTMLElement, 0),
);
});
// WRITE PASS 1: size-lock roots and intermediates so they don't collapse
// when their children become position:fixed.
[...rootSizes, ...intermediates].forEach(({ el, w, h }) => {
this.dirtyElements.push({ el, originalCssText: el.style.cssText });
el.style.width = `${w}px`;
el.style.height = `${h}px`;
});
// READ PASS 2: batch all getBoundingClientRect calls before any leaf writes.
const snapshots = leafElements.map((el) => ({
el,
rect: el.getBoundingClientRect(),
}));
// WRITE PASS 2: apply fixed positioning to leaves using snapshotted rects.
this.bodies = snapshots.map(({ el, rect }) => {
this.dirtyElements.push({ el, originalCssText: el.style.cssText });
el.style.width = `${rect.width}px`;
el.style.height = `${rect.height}px`;
el.style.margin = "0px";
el.style.position = "fixed";
el.style.left = "0px";
el.style.top = "0px";
el.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
const body: PhysicsBody = {
el,
x: rect.left,
y: rect.top,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 5,
width: rect.width,
height: rect.height,
originX: rect.left,
originY: rect.top,
isDragging: false,
startX: 0,
startY: 0,
};
this.attachMouseEvents(body);
return body;
});
this.tick();
}
```
- [ ] **Step 4: Replace the entire `stop()` method**
Find and replace the full `stop()` method with:
```typescript
stop() {
this.isRunning = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
const DURATION = 600;
// Glide each body back to its origin position.
this.bodies.forEach((body) => {
body.el.style.transition = `transform ${DURATION}ms cubic-bezier(0.22, 1, 0.36, 1)`;
body.el.style.transform = `translate(${body.originX}px, ${body.originY}px)`;
});
// After the transition completes, restore every element's original inline
// styles so document flow is fully recovered — including elements that had
// pre-existing inline styles we must not erase (e.g. Next.js fill images).
const elementsToClear = [...this.dirtyElements];
this.bodies = [];
this.dirtyElements = [];
setTimeout(() => {
elementsToClear.forEach(({ el, originalCssText }) => {
el.style.cssText = originalCssText;
});
}, DURATION);
}
```
- [ ] **Step 5: Verify TypeScript compiles with no errors**
```bash
cd /mnt/drive/dev/gabrielkaszewski-next && npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 6: Commit**
```bash
git -C /mnt/drive/dev/gabrielkaszewski-next add lib/gravity-engine.ts
git -C /mnt/drive/dev/gabrielkaszewski-next commit -m "fix: two-pass start, depth cap, cssText restore on stop"
```
---
### Task 2: Visual verification
**Files:** none — browser testing only
- [ ] **Step 1: Start the dev server**
```bash
cd /mnt/drive/dev/gabrielkaszewski-next && npm run dev
```
Open `http://localhost:3000`.
- [ ] **Step 2: Test home page — activation**
Click the gravity toggle (bottom-right, yellow). Verify:
- No freeze or stutter at the moment of activation
- Elements fall and bounce without visible lag
- Hero section text/links fall; background image does NOT move (excluded via `pointer-events-none` class)
- [ ] **Step 3: Test home page — deactivation**
Click toggle again. Verify:
- Elements glide back with spring curve (~600ms)
- Page returns to its original layout — no broken images, no collapsed sections, no leftover inline styles
- Hero background image is still correctly filling the section
- [ ] **Step 4: Test projects page**
Navigate to `http://localhost:3000/projects`. Toggle gravity on/off. Verify:
- Activation is instant (no freeze)
- Animation runs smoothly — project entries fall as chunked units (left panel + image panel), not as hundreds of individual text nodes
- Deactivation restores layout cleanly
- [ ] **Step 5: Test about page**
Navigate to `http://localhost:3000/about`. Toggle gravity on/off. Verify:
- Profile photo glides back and renders correctly after restore
- All sections (hobbies, toolkit, FAQ) restore to original layout
- [ ] **Step 6: Toggle multiple times**
On each page, toggle gravity on/off 3 times in succession. Verify no accumulated style corruption across cycles.
- [ ] **Step 7: Commit any fixes found, or mark done**
If visual issues were found and fixed during testing:
```bash
git -C /mnt/drive/dev/gabrielkaszewski-next add lib/gravity-engine.ts
git -C /mnt/drive/dev/gabrielkaszewski-next commit -m "fix: address visual issues found during gravity verification"
```
If no issues found: skip this step.

View File

@@ -1,339 +0,0 @@
# Gravity Engine Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix lag and container-blowout bugs in `GravityEngine`, and add a spring-curve glide-back animation when gravity is toggled off.
**Architecture:** Cache element dimensions at `start()` to eliminate per-frame layout reflow. Track every element whose inline styles are modified in a `dirtyElements` array so `stop()` can fully restore the DOM. On deactivation, animate leaves back to their origin positions via a CSS transition before clearing all styles.
**Tech Stack:** TypeScript, browser DOM APIs, CSS transitions (`cubic-bezier`)
---
## Files
- **Modify:** `lib/gravity-engine.ts` — only file changed
---
### Task 1: Extend `PhysicsBody` and add `dirtyElements` to `GravityEngine`
**Files:**
- Modify: `lib/gravity-engine.ts:1-17`
- [ ] **Step 1: Replace the `PhysicsBody` interface and class fields**
Open `lib/gravity-engine.ts`. Replace lines 117 with:
```typescript
interface PhysicsBody {
el: HTMLElement;
x: number;
y: number;
vx: number;
vy: number;
width: number;
height: number;
originX: number;
originY: number;
isDragging: boolean;
startX: number;
startY: number;
}
export class GravityEngine {
private bodies: PhysicsBody[] = [];
private dirtyElements: HTMLElement[] = [];
private animationFrameId: number | null = null;
private isRunning = false;
INITIAL_FORCE = 10;
```
- [ ] **Step 2: Verify TypeScript compiles**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add lib/gravity-engine.ts
git commit -m "refactor: extend PhysicsBody with cached dims and add dirtyElements"
```
---
### Task 2: Refactor `start()` into three ordered phases
**Files:**
- Modify: `lib/gravity-engine.ts` — the `start()` method
- [ ] **Step 1: Replace the entire `start()` method**
Find and replace the `start()` method (currently lines 1996) with:
```typescript
start() {
if (this.isRunning) return;
this.isRunning = true;
this.dirtyElements = [];
const containers = document.querySelectorAll(".gravity-body");
const leafElements: HTMLElement[] = [];
// Phase 1 — Size-lock: freeze intermediate container dimensions
// so they don't collapse when children become position:fixed.
const extractLeaves = (el: HTMLElement) => {
const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes(
el.tagName.toUpperCase(),
);
if (isSolid || el.children.length === 0) {
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
leafElements.push(el);
}
return;
}
el.style.width = `${el.offsetWidth}px`;
el.style.height = `${el.offsetHeight}px`;
this.dirtyElements.push(el);
Array.from(el.children).forEach((child) =>
extractLeaves(child as HTMLElement),
);
};
containers.forEach((container) => {
const htmlContainer = container as HTMLElement;
htmlContainer.style.width = `${htmlContainer.offsetWidth}px`;
htmlContainer.style.height = `${htmlContainer.offsetHeight}px`;
this.dirtyElements.push(htmlContainer);
Array.from(htmlContainer.children).forEach((child) =>
extractLeaves(child as HTMLElement),
);
});
// Phase 2 — Read: batch all getBoundingClientRect() before any writes.
const snapshots = leafElements.map((el) => ({
el,
rect: el.getBoundingClientRect(),
}));
// Phase 3 — Write: apply fixed positioning using snapshotted rects.
this.bodies = snapshots.map(({ el, rect }) => {
el.style.width = `${rect.width}px`;
el.style.height = `${rect.height}px`;
el.style.margin = "0px";
el.style.position = "fixed";
el.style.left = "0px";
el.style.top = "0px";
el.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
this.dirtyElements.push(el);
const body: PhysicsBody = {
el,
x: rect.left,
y: rect.top,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 5,
width: rect.width,
height: rect.height,
originX: rect.left,
originY: rect.top,
isDragging: false,
startX: 0,
startY: 0,
};
this.attachMouseEvents(body);
return body;
});
this.tick();
}
```
- [ ] **Step 2: Verify TypeScript compiles**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add lib/gravity-engine.ts
git commit -m "refactor: split start() into size-lock, read, write phases"
```
---
### Task 3: Fix `tick()` — use cached dimensions, remove redundant style sets
**Files:**
- Modify: `lib/gravity-engine.ts` — the `tick` arrow function
- [ ] **Step 1: Replace the `tick` method**
Find and replace the `private tick = () => { ... };` block with:
```typescript
private tick = () => {
if (!this.isRunning) return;
const gravity = 0.5;
const bounce = -0.7;
const floorY = window.innerHeight;
this.bodies.forEach((body) => {
if (body.isDragging) {
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
return;
}
body.vy += gravity;
body.x += body.vx;
body.y += body.vy;
body.vx *= 0.99;
body.vy *= 0.99;
if (body.y + body.height > floorY) {
body.y = floorY - body.height;
body.vy *= bounce;
body.vx *= 0.9;
}
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
});
this.animationFrameId = requestAnimationFrame(this.tick);
};
```
- [ ] **Step 2: Verify TypeScript compiles**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add lib/gravity-engine.ts
git commit -m "perf: use cached body dimensions in tick, remove redundant style sets"
```
---
### Task 4: Implement `stop()` with glide-back animation and dirty-set cleanup
**Files:**
- Modify: `lib/gravity-engine.ts` — the `stop()` method
- [ ] **Step 1: Replace the `stop()` method**
Find and replace the entire `stop()` method with:
```typescript
stop() {
this.isRunning = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
const DURATION = 600;
// Glide each body back to its origin position.
this.bodies.forEach((body) => {
body.el.style.transition = `transform ${DURATION}ms cubic-bezier(0.22, 1, 0.36, 1)`;
body.el.style.transform = `translate(${body.originX}px, ${body.originY}px)`;
});
// After the transition completes, clear all inline styles from every
// element we touched so the document flow is fully restored.
const elementsToClear = [...this.dirtyElements];
this.bodies = [];
this.dirtyElements = [];
setTimeout(() => {
elementsToClear.forEach((el) => {
el.style.transform = "";
el.style.transition = "";
el.style.position = "";
el.style.left = "";
el.style.top = "";
el.style.width = "";
el.style.height = "";
el.style.margin = "";
});
}, DURATION);
}
```
- [ ] **Step 2: Verify TypeScript compiles**
```bash
npx tsc --noEmit
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add lib/gravity-engine.ts
git commit -m "feat: glide-back animation on stop, full dirty-set cleanup"
```
---
### Task 5: Visual verification
**Files:** none — browser testing only
- [ ] **Step 1: Start the dev server**
```bash
npm run dev
```
Open `http://localhost:3000` in a browser.
- [ ] **Step 2: Test activation**
Click the gravity toggle button (bottom-right, yellow). Verify:
- Elements fall and bounce with no visible lag
- Page containers do not collapse or change size while gravity is active
- Elements can be dragged and thrown
- [ ] **Step 3: Test deactivation**
Click the toggle again. Verify:
- All elements glide back to their original positions with a spring curve (~600ms)
- After the animation, the page looks exactly like it did before gravity was activated (no leftover inline styles, no layout shift)
- Toggling on/off multiple times works correctly each time
- [ ] **Step 4: Test the about page**
Navigate to `http://localhost:3000/about`, repeat the activation/deactivation test. The about page also has a `.gravity-body` container with more complex nested layout (flex, prose, grid sections) — verify it also restores cleanly.
- [ ] **Step 5: Commit if any fixes were needed, otherwise done**
```bash
git add lib/gravity-engine.ts
git commit -m "fix: address visual issues found during gravity testing"
```
(Skip this step if no issues were found.)

View File

@@ -1,87 +0,0 @@
# Experience Section — Vertical Timeline Redesign
## Overview
Replace the horizontal card carousel in the Experience section with a full-width vertical timeline. The new design surfaces job descriptions and sub-phase breakdowns directly from static data — no JavaScript required.
## Constraints
- No client-side JavaScript. All content is statically rendered.
- Data lives in `lib/data.ts`; the `Job` type in `lib/types.ts` is extended to support the new fields.
## Data Model Changes
### `lib/types.ts`
Add `JobSubPhase` and extend `Job`:
```typescript
export interface JobSubPhase {
label: string;
start_date: string;
end_date: string | null; // null = present
bullets: string[];
}
export interface Job {
id: number;
position: string;
company: string;
still_working: boolean;
start_date: string;
end_date: string | null;
technologies: string[];
summary?: string; // optional one-liner shown below company name
sub_phases?: JobSubPhase[]; // optional; if absent, no bullets shown
}
```
### `lib/data.ts`
Update all three job entries:
**WPP** — add `summary` + two sub-phases:
- Backend & Infrastructure (Mar 2025 Present)
- Frontend Architecture (Sep 2023 Mar 2025)
**digimonkeys.com** — no changes to content (no summary, no sub_phases).
**GIAP** — add two sub-phases:
- Desktop / Backend (May 2021 Feb 2022)
- Frontend (Feb 2022 Feb 2023)
Bullet text sourced from CV verbatim (slightly trimmed for display).
## Component Changes
### Remove
- `components/experience-card.tsx` — replaced entirely.
### Add
- `components/experience-timeline.tsx` — renders the full timeline list as a server component. Accepts `jobs: Job[]`. Renders each entry as a timeline card with dot, vertical line, header, optional sub-phases, and tech chips. Pure JSX, no `useState`/`useEffect`.
### Update
- Wherever the Experience section renders `ExperienceCard` in a scroll container — replace with `<ExperienceTimeline jobs={jobs} />`. Remove the horizontal scroll wrapper.
## Visual Design
- Vertical connecting line: left-aligned, gradient from accent color to transparent.
- Each entry: glassmorphism card (`bg-white/5`, `border-white/10`, `rounded-2xl`), matching the existing site style.
- Sub-phases rendered as nested cards inside the entry card (`bg-white/[0.03]`, `border-white/[0.07]`, `rounded-xl`).
- Sub-phase header: label in small uppercase accent color + date right-aligned.
- Bullets: `<ul>` with `text-sm text-white/60`.
- Tech chips: existing `<Chip>` component reused.
- Divider line between summary/phases and chips: `border-t border-white/10`.
## Entries (final)
| Job | Sub-phases | Bullets |
|-----|-----------|---------|
| WPP (Sep 2023Present) | Backend & Infra, Frontend Arch | Yes |
| digimonkeys.com (May 2021Present) | None | No |
| GIAP (May 2021Feb 2023) | Desktop/Backend, Frontend | Yes |
Freelance period (Feb 2023Sep 2023) is intentionally excluded.

View File

@@ -1,46 +0,0 @@
# Gravity Engine Fix — Design Spec
**Date:** 2026-04-24
**Branch:** gravity
**Files in scope:** `lib/gravity-engine.ts`
## Problem
Two bugs in the current `GravityEngine`:
1. **Lag**`tick()` calls `el.getBoundingClientRect()` on every body every frame, forcing a layout reflow at 60fps.
2. **Container blowout**`extractLeaves()` sets `width`/`height` on intermediate nodes, but `stop()` only clears styles on leaf bodies and `.gravity-body` roots. Intermediate nodes keep their locked dimensions, breaking layout on deactivation.
## Approach — Option A: Cache + dirty-set + CSS transition glide-back
### Data structures
`PhysicsBody` gains four new fields:
- `width`, `height` — snapshotted from `getBoundingClientRect()` at `start()`, used in `tick()` for floor collision instead of live DOM queries
- `originX`, `originY``rect.left`/`rect.top` at activation time, used as the glide-back transform target
`GravityEngine` gains:
- `dirtyElements: HTMLElement[]` — every element whose inline styles are touched (leaves, intermediate containers, `.gravity-body` roots). `stop()` iterates this single list to fully wipe styles.
### `start()` — three strictly ordered phases
1. **Size-lock phase** — walk `.gravity-body` containers and all descendants via `extractLeaves`. For each intermediate node (non-solid, has children), write `width`/`height` from `offsetWidth`/`offsetHeight` and push to `dirtyElements`. Push `.gravity-body` roots too.
2. **Read phase** — batch all `getBoundingClientRect()` across collected leaf elements before touching any positions. One layout flush, no interleaving.
3. **Write phase** — using snapshotted rects, set each leaf to `position: fixed`, zero `left`/`top`, apply `translate(rect.left, rect.top)`. Cache `width`/`height`/`originX`/`originY`. Push leaf to `dirtyElements`. Attach pointer events.
### `tick()` — one change
Replace `el.getBoundingClientRect().height` with `body.height`. Remove redundant `position`/`left`/`top` re-sets inside the dragging branch (set once at start, never change).
### `stop()` — glide-back then full cleanup
1. Cancel animation frame, set `isRunning = false`.
2. For each body: apply `transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)` and set `transform: translate(originX, originY)`.
3. After `600ms` (`setTimeout`): iterate `dirtyElements`, clear all inline styles (`transform`, `position`, `left`, `top`, `width`, `height`, `margin`, `transition`). Document flow fully restored.
4. Clear `bodies[]` and `dirtyElements[]`.
## Success criteria
- No layout reflow during animation loop
- Toggling gravity off glides all elements back to their original positions with a spring curve
- After glide completes, all intermediate container styles are cleared and layout is indistinguishable from the initial page load