Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cc7c9dd
Add windy particle motion mode as default for Thanos disintegrate
cursoragent Aug 1, 2026
9e08a0b
Stabilize windy stepParticles test with fixed turbulence seed
cursoragent Aug 1, 2026
bf9f10e
Retune windy mode to fire-spark motion with buoyant rise
cursoragent Aug 1, 2026
5688d50
Refactor spark motion: drop Perlin noise, consolidate physics
cursoragent Aug 1, 2026
ac7d54b
Strengthen /deep-code-review: mandate fix-in-place for medium+ issues
cursoragent Aug 1, 2026
aaf71f4
Migrate deep-code-review command to Agent Skill format
cursoragent Aug 1, 2026
16cb506
Improve deep-code-review skill structure and discoverability
cursoragent Aug 1, 2026
ee455e7
Enable deep-code-review skill on casual review requests
cursoragent Aug 1, 2026
2dbdc6f
Rename Thanos disintegrate effect to domDisintegrate (trademark-safe)
cursoragent Aug 1, 2026
052f33d
feat(domDisintegrate): zig-zag spark paths, longer travel, review fixes
cursoragent Aug 1, 2026
d67ef66
feat(domDisintegrate): faster zig-zag sparks with longer TTL
cursoragent Aug 1, 2026
d33e1e3
fix(domDisintegrate): natural per-particle spark turbulence
cursoragent Aug 2, 2026
f1bfe1f
fix(domDisintegrate): sustained upwind spark drift, no ballistic fall
cursoragent Aug 2, 2026
f9527ac
refactor(domDisintegrate): dedupe spark hash, wire worker prewarm
cursoragent Aug 2, 2026
cd1c84d
docs(skills): generalize deep-code-review example (not feature-specific)
cursoragent Aug 2, 2026
629b6d8
Add adaptive quality tiers for dom disintegrate on mobile
cursoragent Aug 2, 2026
f5a7d97
Keep full disintegrate quality on flagship phones like Nothing Phone 3
cursoragent Aug 2, 2026
46b2f33
Optimize dom disintegrate hot path without visual regressions
cursoragent Aug 2, 2026
b68c2e0
Further dom disintegrate perf: warm chunk masks, active list, downsam…
cursoragent Aug 2, 2026
471d668
test(dom-disintegrate): stabilize windy turbulence divergence assertion
cursoragent Aug 3, 2026
9b9ec2f
feat(cookie-consent): use splat particle motion for dismiss effect
cursoragent Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .cursor/commands/deep-code-review.md

This file was deleted.

78 changes: 78 additions & 0 deletions .cursor/skills/deep-code-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
name: deep-code-review
description: Reviews code in scope and applies important/medium fixes plus dead-code and duplication removal in the same session. Use when the user asks to review changes, review the diff/PR/branch, look over their code, requests a code review, /deep-code-review, deep review, code audit, or asks to fix review findings — not report-only.
---

# Deep code review

**Fix in place.** Applies to explicit `/deep-code-review` **and** casual requests like “review the changes”, “look over this PR”, or “check my diff”. Review scope, implement fixes, verify, then summarize. A report without code changes is incomplete when important/medium/dead-code issues were in scope.

## Workflow

Copy and track progress:

```
Review progress:
- [ ] Read scope (branch diff, PR, or pointed files) and direct callers/tests
- [ ] Classify each finding: important / medium / low
- [ ] Fix all important, medium, dead code, and improper duplication
- [ ] Run targeted tests and lint on touched files
- [ ] Commit and push if on a PR branch
- [ ] Reply using the output template below
```

**Scope** = current branch changes, open PR diff, or files the user named. Do not expand scope unless needed to fix a finding safely.

## Fix immediately (non-negotiable)

Implement before finishing — never defer these to a follow-up:

| Category | Fix now |
|----------|---------|
| **Dead code** | Unused exports, functions, types, files, imports, unreachable branches, stale post-refactor helpers |
| **Duplication** | Copy-pasted logic, parallel implementations, repeated magic values, duplicate test helpers (extract when used 2+ times in one feature) |
| **Important** | Bugs, security flaws, races, leaks, silent failures, broken error handling, `as any` / unchecked `!`, data-loss risks |
| **Medium** | Misleading names, weak module boundaries, over-engineering, redundant work, scattered constants, tests that only guard implementation details |

**Low** (subjective style, optional refactors): report only, unless trivial (<5 min). When unsure between medium and low → **medium, fix it**.

## Review focus

- **Patterns** — match `.cursorrules`, `.cursor/rules/*.mdc`, `AGENTS.md`; smallest correct diff (DRY, KISS)
- **Security** — auth boundaries, input validation, secrets, injection, unsafe HTML/DOM
- **Reliability** — failure paths fail visibly (`TRPCError`, snackbar, throw, error boundary — match the feature)
- **Concurrency & resources** — stale closures, missing effect/subscription/timer/worker cleanup
- **Tests** — add regression tests for non-trivial behavior fixes; skip tests that duplicate TypeScript or constants

## Verification

- `pnpm exec vitest run <touched-test-paths>` or `pnpm test` when scope is wide
- `pnpm lint` or ESLint on touched files
- Do **not** ship a report-only response when fixes were possible in scope

## Output template

```markdown
## Fixes applied
- [Dead code / dedup / behavior] — what changed and why

## Left unchanged
- [Low-priority or out-of-scope item] — brief rationale

## Verification
- Tests: …
- Lint: …
```

Use code citations for non-obvious changes. Keep prose concise.

## Example

**Finding:** Two modules duplicate the same `hash01` helper; an exported `prewarmWorker()` is never called.

**Action:** Extract a shared hash util; call `prewarmWorker()` from the existing idle warm-up path; add a regression test if behavior changed.

**Response excerpt:**
> **Fixes applied** — Deduplicated hash helper. Wired dead prewarm export into idle setup. Clarified misleading option JSDoc.
>
> **Verification** — Targeted tests pass; ESLint clean on touched files.
3 changes: 2 additions & 1 deletion .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

How this fits together:

- **`AGENTS.md`** — Next.js doc lookup, pnpm, DB/env, and local dev commands.
- **`AGENTS.md`** — Next.js doc lookup, pnpm, DB/env, local dev commands, slash commands.
- **`.cursor/rules/*.mdc`** — Always-on code style (hook imports, type imports, `useEffect` comments).
- **`.cursor/skills/`** — Agent skills; `deep-code-review` auto-applies on casual review requests (not only `/deep-code-review`).
- **This file** — Architecture, stack, patterns, testing, and feature workflow.

If **Next.js bundled docs** disagree with **`.cursor/rules/`** on style (hooks, types), follow **`.cursor/rules/`** for code in this repo.
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Cursor rules to apply (see each file for full wording):

**Project rules:** See **`.cursorrules`** for stack, architecture, tRPC/Redux boundaries, styling (MUI + Emotion, no Tailwind), testing conventions, and feature workflow. **`.cursor/rules/*.mdc`** adds always-on style rules (React hook imports, type imports, `useEffect` comments).

**Skills:** `deep-code-review` — applies to `/deep-code-review` **and** casual “review the changes” / PR review requests; fix in place (implement important/medium fixes, remove dead or duplicated code in the same session). See `.cursor/skills/deep-code-review/SKILL.md`.

## Cursor Cloud specific instructions

### Services overview
Expand Down
2 changes: 1 addition & 1 deletion src/features/cookieConsent/ui/CookieConsentBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type CookieConsentBannerProps = {
onAcceptAll: (event: React.MouseEvent<HTMLButtonElement>) => void;
onRejectNonEssential: (event: React.MouseEvent<HTMLButtonElement>) => void;
onClose: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Visual surface used by optional dismiss effects (e.g. Thanos disintegrate). */
/** Visual surface used by optional dismiss effects (e.g. DOM disintegrate). */
surfaceRef?: React.Ref<HTMLDivElement>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";

import { CookieConsentBanner } from "#/features/cookieConsent/ui/CookieConsentBanner";
import { useI18nContext } from "#/shared/hooks";
import { useThanosDisintegrate } from "#/shared/ui/effects/thanosDisintegrate";
import { useDomDisintegrate } from "#/shared/ui/effects/domDisintegrate";

type CookieConsentBannerWithDismissEffectProps = {
isSettingsView: boolean;
Expand All @@ -17,7 +17,7 @@ type CookieConsentBannerWithDismissEffectProps = {
};

/**
* Presentation wrapper: persists consent immediately, then plays the Thanos
* Presentation wrapper: persists consent immediately, then plays the DOM
* disintegrate effect while the banner stays mounted for the animation.
*/
export const CookieConsentBannerWithDismissEffect: React.FC<
Expand All @@ -32,8 +32,7 @@ export const CookieConsentBannerWithDismissEffect: React.FC<
}) => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const { targetRef, disintegrate, invalidateCapture } =
useThanosDisintegrate();
const { targetRef, disintegrate, invalidateCapture } = useDomDisintegrate();
const isDismissingRef = useRef(false);
const [frozenSettingsView, setFrozenSettingsView] = useState<boolean | null>(
null,
Expand Down Expand Up @@ -69,6 +68,7 @@ export const CookieConsentBannerWithDismissEffect: React.FC<
clientY: event.clientY,
},
maskMode: "radial",
particleMotionMode: "splat",
zIndex: 1200,
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ vi.mock("#/shared/hooks", async (importOriginal) => {
};
});

vi.mock("#/shared/ui/effects/thanosDisintegrate", () => ({
useThanosDisintegrate: () => ({
vi.mock("#/shared/ui/effects/domDisintegrate", () => ({
useDomDisintegrate: () => ({
targetRef: { current: null },
disintegrate: disintegrateMock,
invalidateCapture: vi.fn(),
Expand Down Expand Up @@ -95,6 +95,7 @@ describe("CookieConsentBannerWithDismissEffect", () => {
expect(disintegrateMock).toHaveBeenCalledWith(
expect.objectContaining({
maskMode: "radial",
particleMotionMode: "splat",
origin: expect.objectContaining({
clientX: expect.any(Number),
clientY: expect.any(Number),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";

import { createTestDisintegrateParticle } from "#/shared/ui/effects/domDisintegrate/__tests__/createTestDisintegrateParticle";
import { createActiveParticleTracker } from "#/shared/ui/effects/domDisintegrate/activeParticles";

describe("createActiveParticleTracker", () => {
it("tracks only released particles for step and draw", () => {
const particles = [
createTestDisintegrateParticle({ releaseTime: 0 }),
createTestDisintegrateParticle({ releaseTime: 0.5 }),
];
const tracker = createActiveParticleTracker(particles);

tracker.syncReleased(0.1);

expect(tracker.getActive()).toHaveLength(1);
expect(tracker.getVisibleCount()).toBe(2);
});

it("drops faded particles from the active list", () => {
const particle = createTestDisintegrateParticle({ releaseTime: 0 });
const tracker = createActiveParticleTracker([particle]);

tracker.syncReleased(0.1);
particle.alpha = 0;

tracker.removeDead();

expect(tracker.getActive()).toHaveLength(0);
expect(tracker.getVisibleCount()).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";

import { createTestDisintegrateParticle } from "#/shared/ui/effects/domDisintegrate/__tests__/createTestDisintegrateParticle";
import { applyWaveOrigin } from "#/shared/ui/effects/domDisintegrate/applyWaveOrigin";

describe("applyWaveOrigin", () => {
it("delays particles farther from the origin", () => {
const particles = [
createTestDisintegrateParticle({ x: 0, y: 0, originX: 0, originY: 0 }),
createTestDisintegrateParticle({ x: 30, y: 0, originX: 30, originY: 0 }),
];

const maxReleaseTime = applyWaveOrigin(particles, { x: 0, y: 0 }, 10);

expect(particles[0]?.releaseTime).toBeGreaterThanOrEqual(0);
expect(particles[0]?.releaseTime).toBeLessThan(0.05);
expect(particles[1]?.releaseTime).toBeGreaterThan(2.5);
expect(maxReleaseTime).toBe(particles[1]?.releaseTime);
});
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
import { describe, expect, it } from "vitest";

import { createTestDisintegrateParticle } from "#/shared/ui/effects/domDisintegrate/__tests__/createTestDisintegrateParticle";
import {
assignParticleReleaseTimes,
resolveEffectiveMaskStrategy,
} from "#/shared/ui/effects/thanosDisintegrate/assignParticleReleaseTimes";
import type { ThanosParticle } from "#/shared/ui/effects/thanosDisintegrate/types";

const createParticle = (
overrides: Partial<ThanosParticle> = {},
): ThanosParticle => ({
x: 0,
y: 0,
originX: 0,
originY: 0,
vx: 0,
vy: 0,
color: "rgb(255, 0, 0)",
alpha: 1,
baseAlpha: 1,
size: 2,
rotation: 0,
rotationSpeed: 0,
drag: 0.96,
fadeStart: 0.5,
fadeDuration: 0.4,
releaseTime: 0,
...overrides,
});
} from "#/shared/ui/effects/domDisintegrate/assignParticleReleaseTimes";

describe("resolveEffectiveMaskStrategy", () => {
it("defaults to wave when a click origin is present", () => {
Expand All @@ -45,8 +23,8 @@ describe("resolveEffectiveMaskStrategy", () => {
describe("assignParticleReleaseTimes", () => {
it("staggers release times from a click-origin wave", () => {
const particles = [
createParticle({ x: 0, y: 0 }),
createParticle({ x: 100, y: 0 }),
createTestDisintegrateParticle({ x: 0, y: 0 }),
createTestDisintegrateParticle({ x: 100, y: 0 }),
];

const maxReleaseTime = assignParticleReleaseTimes(particles, {
Expand All @@ -65,8 +43,8 @@ describe("assignParticleReleaseTimes", () => {

it("spreads release times across a grid strategy", () => {
const particles = [
createParticle({ x: 0, y: 0 }),
createParticle({ x: 90, y: 30 }),
createTestDisintegrateParticle({ x: 0, y: 0 }),
createTestDisintegrateParticle({ x: 90, y: 30 }),
];

assignParticleReleaseTimes(particles, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import type { ThanosParticle } from "#/shared/ui/effects/thanosDisintegrate/types";
import { createTestDisintegrateParticle } from "#/shared/ui/effects/domDisintegrate/__tests__/createTestDisintegrateParticle";

const workerInstances: MockWorker[] = [];

Expand Down Expand Up @@ -52,32 +52,10 @@ class MockWorker {
vi.stubGlobal("Worker", MockWorker as unknown as typeof Worker);

const { buildChunkMaskSequenceAsync, terminateChunkMaskWorker } =
await import("#/shared/ui/effects/thanosDisintegrate/buildChunkMaskSequenceAsync");

const createParticle = (
overrides: Partial<ThanosParticle> = {},
): ThanosParticle => ({
x: 0,
y: 0,
originX: 0,
originY: 0,
vx: 0,
vy: 0,
color: "rgb(255, 0, 0)",
alpha: 1,
baseAlpha: 1,
size: 2,
rotation: 0,
rotationSpeed: 0,
drag: 0.96,
fadeStart: 0.5,
fadeDuration: 0.4,
releaseTime: 0,
...overrides,
});
await import("#/shared/ui/effects/domDisintegrate/buildChunkMaskSequenceAsync");

const maskInput = {
particles: [createParticle({ x: 0, y: 0, releaseTime: 0 })],
particles: [createTestDisintegrateParticle({ x: 0, y: 0, releaseTime: 0 })],
displayWidth: 12,
displayHeight: 6,
particlePadding: 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("captureElementToCanvas", () => {

it("returns a sized canvas when SVG rasterization succeeds", async () => {
const { captureElementToCanvas } =
await import("#/shared/ui/effects/thanosDisintegrate/captureElementToCanvas");
await import("#/shared/ui/effects/domDisintegrate/captureElementToCanvas");

const element = document.createElement("div");
element.style.backgroundColor = "rgba(255, 255, 255, 0.55)";
Expand Down Expand Up @@ -67,7 +67,7 @@ describe("captureElementToCanvas", () => {

it("throws when rasterization fails", async () => {
const { captureElementToCanvas } =
await import("#/shared/ui/effects/thanosDisintegrate/captureElementToCanvas");
await import("#/shared/ui/effects/domDisintegrate/captureElementToCanvas");

const element = document.createElement("div");
element.getBoundingClientRect = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
collectDocumentFontFaceCss,
sanitizeEmbeddedStyleCss,
} from "#/shared/ui/effects/thanosDisintegrate/collectDocumentFontFaceCss";
} from "#/shared/ui/effects/domDisintegrate/collectDocumentFontFaceCss";

describe("collectDocumentFontFaceCss", () => {
afterEach(() => {
Expand Down
Loading