Showing preview only (335K chars total). The displayed content is truncated. Use the JSON API for full output.
Repository: kitlangton/visual-effect
Branch: main
Commit: 2f910e4a2fe2
Files: 109
Total size: 309.2 KB
Directory structure:
gitextract_q3jrgzjr/
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── README.md
├── app/
│ ├── ClientAppContent.tsx
│ ├── [exampleId]/
│ │ └── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── biome.json
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.mjs
├── public/
│ ├── _headers
│ ├── generate-favicons.html
│ ├── robots.txt
│ ├── site.webmanifest
│ └── sitemap.xml
├── scripts/
│ └── generate-og-images.tsx
├── src/
│ ├── AppContent.tsx
│ ├── VisualEffect.test.ts
│ ├── VisualEffect.ts
│ ├── VisualRef.ts
│ ├── VisualScope.ts
│ ├── animations.ts
│ ├── components/
│ │ ├── CodeBlock.tsx
│ │ ├── HeaderView.tsx
│ │ ├── ScheduleTimeline.tsx
│ │ ├── Timer.tsx
│ │ ├── display/
│ │ │ ├── EffectExample.tsx
│ │ │ ├── RefDisplay.tsx
│ │ │ └── index.ts
│ │ ├── effect/
│ │ │ ├── EffectContainer.tsx
│ │ │ ├── EffectContent.tsx
│ │ │ ├── EffectLabel.tsx
│ │ │ ├── EffectNode.tsx
│ │ │ ├── EffectOverlay.tsx
│ │ │ ├── index.ts
│ │ │ ├── nodeVariants.ts
│ │ │ ├── taskUtils.ts
│ │ │ └── useEffectMotion.ts
│ │ ├── feedback/
│ │ │ ├── DeathBubble.tsx
│ │ │ ├── EffectLogo.tsx
│ │ │ ├── FailureBubble.tsx
│ │ │ ├── FloatingHighlight.tsx
│ │ │ ├── NotificationBubble.tsx
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── layout/
│ │ │ ├── NavigationSidebar.tsx
│ │ │ └── PageHeader.tsx
│ │ ├── renderers/
│ │ │ ├── ArrayResult.tsx
│ │ │ ├── BasicRenderers.tsx
│ │ │ ├── EmojiResult.tsx
│ │ │ ├── RenderableResult.ts
│ │ │ ├── TemperatureResult.tsx
│ │ │ └── index.ts
│ │ ├── scope/
│ │ │ ├── FinalizerCard.tsx
│ │ │ ├── ScopeStack.tsx
│ │ │ └── utils.ts
│ │ └── ui/
│ │ ├── QuickOpen.tsx
│ │ ├── SegmentedControl.tsx
│ │ ├── VolumeToggle.tsx
│ │ └── index.ts
│ ├── constants/
│ │ ├── colors.ts
│ │ └── dimensions.ts
│ ├── examples/
│ │ ├── effect-acquire-release.tsx
│ │ ├── effect-add-finalizer.tsx
│ │ ├── effect-all-short-circuit.tsx
│ │ ├── effect-all.tsx
│ │ ├── effect-die.tsx
│ │ ├── effect-eventually.tsx
│ │ ├── effect-fail.tsx
│ │ ├── effect-firstsuccessof.tsx
│ │ ├── effect-foreach.tsx
│ │ ├── effect-orelse.tsx
│ │ ├── effect-partition.tsx
│ │ ├── effect-promise.tsx
│ │ ├── effect-race.tsx
│ │ ├── effect-raceall.tsx
│ │ ├── effect-repeat-spaced.tsx
│ │ ├── effect-repeat-while-output.tsx
│ │ ├── effect-retry-exponential.tsx
│ │ ├── effect-retry-recurs.tsx
│ │ ├── effect-sleep.tsx
│ │ ├── effect-succeed.tsx
│ │ ├── effect-sync.tsx
│ │ ├── effect-timeout.tsx
│ │ ├── effect-validate.tsx
│ │ ├── helpers.ts
│ │ ├── ref-make.tsx
│ │ └── ref-update-and-get.tsx
│ ├── hooks/
│ │ ├── useOptionKey.ts
│ │ ├── useStateTransition.ts
│ │ ├── useVisualEffects.ts
│ │ └── useVisualScope.ts
│ ├── lib/
│ │ ├── example-types.ts
│ │ └── examples-manifest.ts
│ ├── shared/
│ │ ├── appItems.ts
│ │ └── idUtils.ts
│ ├── sounds/
│ │ └── TaskSounds.ts
│ └── theme.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.scripts.json
├── vitest.config.ts
└── wrangler.jsonc
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
.next
out/
.vscode
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vercel
# Generated OG images
public/og/
tsconfig.tsbuildinfo
.mcp.json
.claude
================================================
FILE: CLAUDE.md
================================================
# Visual Effect - Codebase Documentation
## Overview
Visual Effect is an interactive visualization tool for the Effect library that demonstrates how Effect operations execute over time. Built with Next.js 15 and React 19, it provides animated visual representations of Effect constructors and combinators with synchronized sound effects, making it easier to understand their behavior.
**In this house, we use bun.** All package management and script execution should use `bun` commands, not `npm` or `node`.
## Core Concepts
### 1. VisualEffect
The `VisualEffect` class is the heart of the visualization system. It wraps Effect operations and tracks their execution state for visualization purposes.
```typescript
// Creating a visual effect
const myEffect = visualEffect("taskName", Effect.succeed(42));
```
Key features:
- **State tracking**: idle → running → completed/failed/interrupted/death
- **Observable hooks**: React components subscribe via `useVisualEffectState`, `useVisualEffectNotification`, or `useVisualEffectSubscription`
- **Effect caching**: Prevents re-execution of already completed effects
- **Timer support**: Captures start/end timestamps when `showTimer` is enabled
- **Notification helpers**: Effects can publish contextual messages through `notify(...)`
- **Sound triggers**: Automatically plays sounds on state transitions
### 2. EffectNode Component
The `EffectNode` component renders individual effects as animated circles with:
- Different colors for different states (idle, running, completed, failed)
- Pulsing animations during execution
- Result display using the renderer system
- Automatic width expansion when results overflow the default size
- Overlay feedback for errors and notifications
### 3. Renderer System
Results are displayed using a flexible renderer pattern:
```typescript
class MyResult implements RenderableResult {
constructor(public value: any) {}
render() {
return <div>{this.value}</div>;
}
}
```
Built-in renderers:
- `NumberResult` - Simple number display
- `StringResult` - Simple string display
- `BooleanResult` - True/false text badge
- `TemperatureResult` - Temperature with a trailing ° symbol
- `ObjectResult` - JSON stringified objects
- `ArrayResult` - Animated array summary (length indicator)
- `EmojiResult` - Emoji-based results with enhanced visual appeal
### 4. Effect Examples
Each example follows a consistent pattern:
```typescript
export function EffectExampleName() {
// 1. Create individual effects with memoization
const effect1 = useMemo(() => visualEffect("name", effect), []);
// 2. Create composed effect if needed
const resultEffect = useMemo(() => {
const composed = Effect.all([effect1.effect, effect2.effect]);
return new VisualEffect("result", composed, [effect1, effect2]);
}, [effect1, effect2]);
// 3. Define code snippet and highlight mappings
const codeSnippet = `...`;
const effectHighlightMap = { ... };
// 4. Return EffectExample component
return <EffectExample ... />;
}
```
## Key Patterns
### 1. Jittered Delays
All examples use realistic, non-deterministic delays to simulate real-world conditions:
```typescript
export function getWeather(location?: string) {
return Effect.gen(function* () {
const delay = getDelay(500, 900); // Random 500-900ms
yield* Effect.sleep(delay);
return new TemperatureResult(...);
});
}
```
### 2. Responsive Design
- Layout built with Tailwind utility classes and Motion; flex containers wrap naturally on small screens
- Sidebar navigation collapses on narrow viewports while the main content remains accessible
- Typography and spacing scale using relative units for readability across devices
### 3. State Management
- Each `VisualEffect` manages its own state
- React components subscribe via `useVisualEffectState`, `useVisualEffectNotification`, or `useVisualEffectSubscription`
- Lightweight hooks (`useOptionKey`, `useStateTransition`, `useVisualScope`) handle UI-specific state
- No global state management for effect execution
- Effects persist across component re-renders
### 4. Animation System
- Uses Motion (Framer Motion successor) for smooth transitions
- Spring animations for natural movement with configurable physics
- Different animations for different state transitions
- Hardware-accelerated transforms
- Dedicated sequences for running jitter, failure shakes, and death glitches
### 5. Sound System
The application includes a synthesized sound system using Tone.js:
- **Distinct cues**: Success, running, failure, interruption, reset, death, ref updates, finalizers, and notifications all receive unique tones
- **Shared processing**: A centralized `taskSounds` module initializes synths, routing, and reverb once and gates playback behind a mute flag
- **User controls**: The header exposes an ON/OFF toggle that updates the mute state and plays a confirmation chime when sound is enabled
- **Integration**: `VisualEffect.setState()` and companion helpers trigger the appropriate cues during state transitions
## File Structure
```
app/ # Next.js App Router
├── layout.tsx # Root layout with metadata
├── page.tsx # Home page
├── [exampleId]/
│ └── page.tsx # Individual example pages
└── ClientAppContent.tsx # Client-side app content
src/
├── animations.ts # Shared animation tokens
├── AppContent.tsx # Main app component
├── components/
│ ├── CodeBlock.tsx # Syntax-highlighted code
│ ├── HeaderView.tsx # Example headers + controls
│ ├── ScheduleTimeline.tsx # Scheduling visualizer
│ ├── Timer.tsx # Elapsed time labels
│ ├── display/ # Display components
│ │ ├── EffectExample.tsx # Main example wrapper
│ │ └── RefDisplay.tsx # Ref visualizations
│ ├── effect/ # Effect visualization primitives
│ │ ├── EffectNode.tsx
│ │ ├── EffectOverlay.tsx
│ │ ├── taskUtils.ts
│ │ └── useEffectMotion.ts
│ ├── feedback/ # User feedback
│ │ ├── DeathBubble.tsx
│ │ ├── FailureBubble.tsx
│ │ └── NotificationBubble.tsx
│ ├── layout/ # Layout components
│ │ ├── NavigationSidebar.tsx
│ │ └── PageHeader.tsx
│ ├── renderers/ # Result rendering system
│ │ ├── ArrayResult.tsx
│ │ ├── BasicRenderers.tsx
│ │ ├── EmojiResult.tsx
│ │ └── TemperatureResult.tsx
│ ├── scope/
│ │ ├── FinalizerCard.tsx
│ │ └── ScopeStack.tsx
│ └── ui/
│ ├── QuickOpen.tsx
│ ├── SegmentedControl.tsx
│ └── VolumeToggle.tsx
├── constants/
│ ├── colors.ts
│ └── dimensions.ts
├── examples/ # Effect examples
│ ├── helpers.ts # Shared utilities
│ ├── effect-*.tsx # Effect examples
│ └── ref-*.tsx # Ref examples
├── hooks/ # Custom hooks
│ ├── useOptionKey.ts # Option key detection
│ ├── useStateTransition.ts # Effect transition tracking
│ └── useVisualScope.ts # Scope management
├── lib/ # Library code
│ ├── example-types.ts # Type definitions
│ └── examples-manifest.ts # Example registry
├── shared/ # Shared utilities
│ ├── appItems.ts
│ └── idUtils.ts
├── sounds/
│ └── TaskSounds.ts # Synthesized sound system
├── theme.ts # Theme tokens
├── VisualEffect.ts # Core effect visualization
├── VisualRef.ts # Ref visualization
├── VisualScope.ts # Scope visualization
└── VisualEffect.test.ts # Unit tests
```
## Design Decisions
### 1. No External State Management
Each task manages its own state internally, avoiding complexity and making examples self-contained.
### 2. Effect-First Design
The visualization follows Effect's execution model closely - tasks only run when their effect is executed.
### 3. Realistic Timing
All examples use jittered delays to demonstrate non-deterministic behavior, especially important for race conditions.
### 4. Mobile-Responsive
The entire UI adapts to mobile screens without compromising the desktop experience.
### 5. Type Safety
Strict TypeScript configuration catches errors at compile time, including:
- `isolatedModules` for Next.js compatibility
- `noUncheckedIndexedAccess` for array safety
- `exactOptionalPropertyTypes` for precise optional handling
- Effect-specific TypeScript plugin for enhanced type checking
### 6. Audio Experience
Sounds are designed to enhance understanding without being intrusive:
- Short, focused cues map directly to running, completion, failure, interruption, and reset events
- A centralized sound module keeps the palette cohesive and manages initialization/muting
- Automatic sound on state transitions with respectful default levels
- User-friendly mute control without in-app volume sliders (defers to system volume)
## Common Operations
### Adding a New Effect Example
1. Create a new file in `src/examples/`
2. Use the `getWeather` helper for consistent behavior
3. Follow the example pattern (memoized effects, code snippet, highlight map)
4. Add to the examples manifest in `src/lib/examples-manifest.ts`
5. Generate OG images with `bun run generate-og-images`
### Creating Custom Renderers
1. Implement the `RenderableResult` interface in `src/components/renderers/`
2. Add a `render()` method returning JSX
3. Export from the renderers index file
4. Use in your effect: `Effect.map(value => new MyRenderer(value))`
### Modifying Animations
Look in `EffectNode.tsx` and `animations.ts` for animation configurations:
- Spring settings including `defaultSpring` for MotionConfig
- Color transitions in state change logic
- Timing constants in individual components
- All animation tokens centralized in `animations.ts`
## Best Practices
1. **Always memoize effects** - Prevents recreation on every render
2. **Use built-in helpers** - `getWeather()` for consistency
3. **Keep effects pure** - Side effects only for visualization
4. **Test on mobile** - Ensure responsive behavior works
5. **Follow the pattern** - Consistency makes the codebase maintainable
6. **Use proper accessibility** - Provide ARIA labels, focus states, and keyboard-friendly controls
7. **Optimize bundle size** - Lazy load examples and use code splitting
## Copy Style Guide
### Text and Descriptions
**Example Descriptions:**
- Use imperative mood (e.g., "Create", "Run", "Compose")
- No ending punctuation (periods, exclamation marks)
- Start with action verbs for consistency
- Keep descriptions concise but informative
**Good Examples:**
- "Run multiple effects concurrently and compose their results"
- "Interrupt a running effect after a specified duration"
- "Accumulate validation errors instead of failing fast"
**Avoid:**
- Present tense ("Creates", "Runs", "Composes")
- Ending punctuation ("Create a new task.")
- Passive voice ("A task is created")
**UI Text:**
- Use proper articles (a, an, the) in prose
- Maintain consistent tone throughout the application
- Keep instructions clear and action-oriented
- Use sentence case for buttons and labels
**Error Messages:**
- Start with the action or context
- Be specific about what went wrong
- Provide actionable next steps when possible
**Code Comments:**
- Use present tense for describing what code does
- Keep comments concise and focused on the "why"
- Avoid obvious comments that just restate the code
## Architecture Decisions
### 1. Next.js App Router
Migrated from Vite to Next.js 15 for:
- Better SEO with server-side rendering
- Individual pages for each example
- Automatic code splitting and optimization
- Built-in image optimization
### 2. Component Architecture
Organized components by domain:
- `display/` - Main display logic
- `effect/` - Effect visualization specifics
- `feedback/` - User feedback components
- `layout/` - Layout and navigation
- `renderers/` - Result rendering system
- `scope/` - Scope and finalizer visualization
- `ui/` - Reusable UI components
- Top-level helpers (`CodeBlock`, `HeaderView`, `Timer`, `ScheduleTimeline`) live alongside these folders
### 3. New Visualization Types
Expanded beyond basic effects to include:
- **Ref visualization** with `VisualRef` class
- **Scope visualization** with finalizer tracking
- **Quick-open modal** and link-copy affordances for faster exploration
- **Lazy loading** for better performance
### 4. Enhanced Developer Experience
- Biome for linting and formatting (replaced ESLint/Prettier)
- TypeScript strict mode with Effect language service
- Automated OG image generation for social sharing
- Comprehensive example manifest system
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Kit Langton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Visual Effect
Interactive visualizations for [Effect](https://github.com/Effect-TS/effect) programs, built with Effect itself.
## What is this?
An interactive Next.js app that visualizes how Effect operations execute over time. Watch effects run, compose, race, and handle errors with animated nodes and synchronized sound feedback.
## Local Development
```bash
pnpm install
pnpm dev
```
## License
MIT — see LICENSE
================================================
FILE: app/ClientAppContent.tsx
================================================
"use client"
import dynamic from "next/dynamic"
const AppContent = dynamic(
() => import("../src/AppContent").then(mod => ({ default: mod.AppContent })),
{
ssr: false,
},
)
export default function ClientAppContent() {
return <AppContent />
}
================================================
FILE: app/[exampleId]/page.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import type { Metadata } from "next"
import { examplesManifest, getExampleMeta } from "../../src/lib/examples-manifest"
export const dynamicParams = false
// Build-time helper
export async function generateStaticParams() {
return examplesManifest.map(example => ({ exampleId: example.id }))
}
// Dynamic metadata generation for each example route
export async function generateMetadata({
params,
}: {
params: Promise<{ exampleId: string }>
}): Promise<Metadata> {
const { exampleId } = await params
try {
const meta = getExampleMeta(exampleId)
if (!meta) {
return {
title: "Example Not Found - Visual Effect",
description: "The requested example could not be found",
}
}
const title = `${meta.name}${meta.variant ? ` ${meta.variant}` : ""} - Visual Effect`
return {
title,
description: meta.description,
openGraph: {
title,
description: meta.description,
url: `https://effect.kitlangton.com/${exampleId}`,
siteName: "Visual Effect",
images: [
{
url: `/og/${exampleId}.png`,
width: 1200,
height: 630,
alt: title,
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title,
description: meta.description,
images: [`/og/${exampleId}.png`],
},
}
} catch {
// Fallback to default metadata
return {
title: "Visual Effect - Interactive Effect Playground",
description: "Interactive examples of TypeScript's beautiful Effect library",
}
}
}
export default function ExamplePage() {
return null // nothing mounts, so no state loss
}
================================================
FILE: app/globals.css
================================================
@import "tailwindcss";
/* Dark mode scrollbar for all elements */
* {
scrollbar-width: thin;
scrollbar-color: #262626 #0a0a0a;
}
/* Webkit scrollbar styling */
.sidebar-scrollbar::-webkit-scrollbar,
body::-webkit-scrollbar,
html::-webkit-scrollbar,
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.sidebar-scrollbar::-webkit-scrollbar-track,
body::-webkit-scrollbar-track,
html::-webkit-scrollbar-track,
*::-webkit-scrollbar-track {
background: #0a0a0a;
}
.sidebar-scrollbar::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb,
html::-webkit-scrollbar-thumb,
*::-webkit-scrollbar-thumb {
background: #262626;
border-radius: 3px;
}
.sidebar-scrollbar::-webkit-scrollbar-thumb:hover,
body::-webkit-scrollbar-thumb:hover,
html::-webkit-scrollbar-thumb:hover,
*::-webkit-scrollbar-thumb:hover {
background: #404040;
}
================================================
FILE: app/layout.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import "./globals.css"
import ClientAppContent from "./ClientAppContent"
export const metadata = {
title: "Visual Effect - Interactive Effect Playground",
description:
"An interactive visualization tool for the Effect library that demonstrates how Effect operations execute over time with animated visual representations and synchronized sound effects.",
metadataBase: new URL("https://effect.kitlangton.com"),
openGraph: {
title: "Visual Effect - Interactive Effect Playground",
description: "Interactive examples of TypeScript's beautiful Effect library",
url: "https://effect.kitlangton.com/",
siteName: "Visual Effect",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Visual Effect - Interactive Effect Playground",
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Visual Effect - Interactive Effect Playground",
description: "Interactive examples of TypeScript's beautiful Effect library",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
verification: {
google: "google-verification-code", // Add your Google verification code if needed
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="bg-neutral-950 text-white">
{/* `vsc-initialized` is injected by some VS Code extensions after SSR; suppress hydration mismatch warnings */}
<body>
<ClientAppContent />
{children}
</body>
</html>
)
}
================================================
FILE: app/page.tsx
================================================
export default function HomePage() {
return null
}
================================================
FILE: biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noExplicitAny": "warn"
},
"style": {
"noNonNullAssertion": "warn",
"useNodejsImportProtocol": "error"
},
"complexity": {
"useLiteralKeys": "error"
},
"a11y": {
"noStaticElementInteractions": "off",
"noSvgWithoutTitle": "off",
"useKeyWithClickEvents": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"quoteStyle": "double",
"jsxQuoteStyle": "double",
"trailingCommas": "all",
"arrowParentheses": "asNeeded"
}
},
"json": {
"parser": {
"allowComments": true
}
},
"files": {
"includes": [
"**",
"!**/node_modules",
"!**/out",
"!**/dist",
"!**/.next",
"!**/coverage",
"!**/*.min.js",
"!**/pnpm-lock.yaml"
]
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
}
}
================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./out/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {},
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
distDir: "out",
reactStrictMode: true,
experimental: {
esmExternals: true,
},
webpack: (config, { isServer }) => {
// Disable module concatenation to fix "Unexpected end of JSON input" errors
config.optimization.concatenateModules = false
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false,
}
}
return config
},
transpilePackages: ["motion"],
}
export default nextConfig
================================================
FILE: package.json
================================================
{
"name": "visual-effect",
"private": true,
"version": "0.0.0",
"type": "module",
"sideEffects": false,
"license": "MIT",
"description": "Interactive visual state machine for Effect (effect-ts) with a Next.js + React demo and Tone.js sound cues.",
"repository": {
"type": "git",
"url": "https://github.com/kitlangton/visual-effect.git"
},
"homepage": "https://github.com/kitlangton/visual-effect#readme",
"bugs": {
"url": "https://github.com/kitlangton/visual-effect/issues"
},
"author": "Kit Langton",
"keywords": [
"effect",
"effect-ts",
"react",
"nextjs",
"tonejs",
"state-machine",
"visualization",
"typescript"
],
"engines": {
"node": ">=20",
"bun": ">=1.0.0"
},
"scripts": {
"dev": "next dev",
"prebuild": "tsx scripts/generate-og-images.tsx",
"build": "next build",
"cf:build": "bun run build",
"cf:deploy": "wrangler deploy",
"deploy": "bun run cf:build && bun run cf:deploy",
"start": "next start",
"lint": "biome lint .",
"lint-fix": "biome lint . --apply",
"format": "biome format . --write",
"check": "biome check .",
"check-fix": "biome check . --write",
"preview": "next start",
"test": "vitest",
"typecheck": "tsc --noEmit",
"type-check": "tsc --noEmit",
"generate-og-images": "tsx scripts/generate-og-images.tsx",
"fix": "biome check . --write && bun run typecheck",
"clean": "biome format . --write && biome lint . --write && bun run typecheck",
"verify": "biome check . && bun run typecheck && bun run test",
"ready": "bun run clean && bun run build"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"effect": "^3.19.14",
"motion": "^12.27.1",
"next": "^16.1.3",
"prism-react-renderer": "^2.4.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tone": "^15.1.22"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@effect/language-service": "^0.71.0",
"@resvg/resvg-js": "^2.6.2",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.9",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitest/ui": "^4.0.17",
"jsdom": "^27.4.0",
"satori": "^0.19.1",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "~5.9.3",
"vitest": "^4.0.17",
"wrangler": "^4.107.0"
},
"packageManager": "bun@1.3.0",
"trustedDependencies": [
"@tailwindcss/oxide"
]
}
================================================
FILE: postcss.config.mjs
================================================
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}
================================================
FILE: public/_headers
================================================
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
/_next/static/*
Cache-Control: public, max-age=31536000, immutable
/og/*
Cache-Control: public, immutable, no-transform, max-age=31536000
/fonts/*
Cache-Control: public, immutable, max-age=31536000
================================================
FILE: public/generate-favicons.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Generate Favicons</title>
</head>
<body>
<h1>Generate PNG Favicons from SVG</h1>
<p>Open this file in a browser, then right-click and save each image:</p>
<h2>favicon-32x32.png</h2>
<img src="/favicon.svg" width="32" height="32" style="image-rendering: crisp-edges; background: black;">
<h2>favicon-16x16.png</h2>
<img src="/favicon.svg" width="16" height="16" style="image-rendering: crisp-edges; background: black;">
<h2>apple-touch-icon.png (180x180)</h2>
<img src="/favicon.svg" width="180" height="180" style="image-rendering: crisp-edges; background: black;">
<h2>android-chrome-192x192.png</h2>
<img src="/favicon.svg" width="192" height="192" style="image-rendering: crisp-edges; background: black;">
<h2>android-chrome-512x512.png</h2>
<img src="/favicon.svg" width="512" height="512" style="image-rendering: crisp-edges; background: black;">
<script>
// You can use this script with a canvas to generate PNGs programmatically
// For now, just save these manually or use an online converter
</script>
</body>
</html>
================================================
FILE: public/robots.txt
================================================
# robots.txt for Visual Effect
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://effect.kitlangton.com/sitemap.xml
================================================
FILE: public/site.webmanifest
================================================
{
"name": "Visual Effect - Interactive Effect Library Visualizer",
"short_name": "Visual Effect",
"description": "An interactive visualization tool for the Effect library that demonstrates how Effect operations execute over time with animated visual representations and synchronized sound effects.",
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone",
"start_url": "/",
"scope": "/",
"orientation": "portrait-primary"
}
================================================
FILE: public/sitemap.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://effect.kitlangton.com/</loc>
<lastmod>2025-01-05</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
================================================
FILE: scripts/generate-og-images.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import fs from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { Resvg } from "@resvg/resvg-js"
// biome-ignore lint/correctness/noUnusedImports: We actually need it, liar.
import React from "react"
import satori from "satori"
import { examplesManifest } from "../src/lib/examples-manifest.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// No need for createExampleId anymore - we use the id from meta
// OG Image Component
function OGImage({
description,
name,
variant,
}: {
name: string
variant?: string
description: string
}) {
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: 80,
background: "linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%)",
fontFamily: "Inter",
}}
>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 20 }}>
<div
style={{
width: 60,
height: 60,
borderRadius: 12,
background: "#3b82f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width="32" height="32" fill="white" viewBox="0 0 256 256">
<path d="M240,128a15.79,15.79,0,0,1-10.5,15l-63.44,23.07L143,229.5a16,16,0,0,1-30,0L89.94,166.06,26.5,143a16,16,0,0,1,0-30L89.94,89.94,113,26.5a16,16,0,0,1,30,0l23.07,63.44L229.5,113A15.79,15.79,0,0,1,240,128Z"></path>
</svg>
</div>
<div
style={{
color: "#6b7280",
fontSize: 24,
fontWeight: 700,
letterSpacing: "0.05em",
}}
>
VISUAL EFFECT
</div>
</div>
{/* Main Content */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 20,
flex: 1,
justifyContent: "center",
}}
>
<div style={{ display: "flex", alignItems: "baseline", gap: 16 }}>
<h1
style={{
fontSize: 72,
color: "white",
margin: 0,
fontWeight: 700,
lineHeight: 1,
}}
>
{name}
</h1>
{variant && (
<span
style={{
fontSize: 48,
color: "#6b7280",
fontWeight: 500,
}}
>
{variant}
</span>
)}
</div>
<p
style={{
fontSize: 32,
color: "#9ca3af",
margin: 0,
lineHeight: 1.4,
maxWidth: "90%",
}}
>
{description}
</p>
</div>
{/* Footer */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
paddingTop: 40,
borderTop: "1px solid #374151",
}}
>
<div style={{ color: "#6b7280", fontSize: 20 }}>Interactive Effect Examples</div>
<div style={{ color: "#6b7280", fontSize: 20 }}>effect.kitlangton.com</div>
</div>
</div>
)
}
async function generateOGImages() {
console.log("🎨 Generating Open Graph images...")
// Ensure output directory exists
const outputDir = path.join(__dirname, "..", "public", "og")
await fs.mkdir(outputDir, { recursive: true })
// Load font
const interBoldPath = path.join(__dirname, "..", "public", "fonts", "Inter-Bold.ttf")
const interRegularPath = path.join(__dirname, "..", "public", "fonts", "Inter-Regular.ttf")
// Check if fonts exist, if not, download them
let interBold: ArrayBuffer
let interRegular: ArrayBuffer
try {
const boldBuffer = await fs.readFile(interBoldPath)
const regularBuffer = await fs.readFile(interRegularPath)
interBold = boldBuffer.buffer.slice(
boldBuffer.byteOffset,
boldBuffer.byteOffset + boldBuffer.byteLength,
) as ArrayBuffer
interRegular = regularBuffer.buffer.slice(
regularBuffer.byteOffset,
regularBuffer.byteOffset + regularBuffer.byteLength,
) as ArrayBuffer
} catch {
console.log("⚠️ Fonts not found locally, using system fonts as fallback")
// Use a minimal font as fallback
interBold = new ArrayBuffer(0)
interRegular = new ArrayBuffer(0)
}
// Generate all images in parallel
const imagePromises = examplesManifest.map(async example => {
const id = example.id
console.log(` 📸 Generating ${id}.png`)
try {
const svg = await satori(
<OGImage
name={example.name}
{...(example.variant ? { variant: example.variant } : {})}
description={example.description}
/>,
{
width: 1200,
height: 630,
fonts:
interBold.byteLength > 0
? [
{
name: "Inter",
data: interBold,
weight: 700,
style: "normal",
},
{
name: "Inter",
data: interRegular,
weight: 400,
style: "normal",
},
]
: [],
},
)
const resvg = new Resvg(svg, {
fitTo: {
mode: "width",
value: 1200,
},
})
const pngData = resvg.render()
const pngBuffer = pngData.asPng()
await fs.writeFile(path.join(outputDir, `${id}.png`), pngBuffer)
return { id, success: true }
} catch (error) {
console.error(` ❌ Failed to generate ${id}.png:`, error)
return { id, success: false, error }
}
})
// Wait for all images to complete
const results = await Promise.allSettled(imagePromises)
// Count successes and failures
const completed = results.filter(r => r.status === "fulfilled").length
const failed = results.filter(r => r.status === "rejected").length
if (failed > 0) {
console.log(`⚠️ ${failed} images failed to generate`)
}
console.log(`✅ Generated ${completed} Open Graph images successfully`)
}
// Run the script
generateOGImages().catch(console.error)
================================================
FILE: src/AppContent.tsx
================================================
"use client"
import {
ArrowClockwiseIcon,
HashStraightIcon,
HeartIcon,
PlayIcon,
SkullIcon,
StopIcon,
} from "@phosphor-icons/react"
import { MotionConfig } from "motion/react"
import { usePathname } from "next/navigation"
import { Fragment, useCallback, useEffect, useMemo, useState } from "react"
// Examples
import EffectAcquireRelease from "@/examples/effect-acquire-release"
import EffectAddFinalizer from "@/examples/effect-add-finalizer"
import EffectAll from "@/examples/effect-all"
import EffectAllShortCircuit from "@/examples/effect-all-short-circuit"
import EffectDie from "@/examples/effect-die"
import EffectEventually from "@/examples/effect-eventually"
import EffectFail from "@/examples/effect-fail"
import EffectFirstSuccessOf from "@/examples/effect-firstsuccessof"
import EffectForEach from "@/examples/effect-foreach"
import EffectOrElse from "@/examples/effect-orelse"
import EffectPartition from "@/examples/effect-partition"
import EffectPromise from "@/examples/effect-promise"
import EffectRace from "@/examples/effect-race"
import EffectRaceAll from "@/examples/effect-raceall"
import EffectRepeatSpaced from "@/examples/effect-repeat-spaced"
import EffectRepeatWhileOutput from "@/examples/effect-repeat-while-output"
import EffectRetryExponential from "@/examples/effect-retry-exponential"
import EffectRetryRecurs from "@/examples/effect-retry-recurs"
import EffectSleep from "@/examples/effect-sleep"
import EffectSucceed from "@/examples/effect-succeed"
import EffectSync from "@/examples/effect-sync"
import EffectTimeout from "@/examples/effect-timeout"
import EffectValidate from "@/examples/effect-validate"
import RefMake from "@/examples/ref-make"
import RefUpdateAndGet from "@/examples/ref-update-and-get"
import type { ExampleMeta } from "@/lib/example-types"
type ExampleComponent = React.ComponentType<{
index: number
metadata: ExampleMeta
exampleId: string
}>
const exampleComponentById: Record<string, ExampleComponent> = {
"effect-acquire-release": EffectAcquireRelease,
"effect-add-finalizer": EffectAddFinalizer,
"effect-all-short-circuit": EffectAllShortCircuit,
"effect-all": EffectAll,
"effect-die": EffectDie,
"effect-eventually": EffectEventually,
"effect-firstsuccessof": EffectFirstSuccessOf,
"effect-fail": EffectFail,
"effect-foreach": EffectForEach,
"effect-orelse": EffectOrElse,
"effect-partition": EffectPartition,
"effect-promise": EffectPromise,
"effect-race": EffectRace,
"effect-raceall": EffectRaceAll,
"effect-repeat-spaced": EffectRepeatSpaced,
"effect-repeat-while-output": EffectRepeatWhileOutput,
"effect-retry-exponential": EffectRetryExponential,
"effect-retry-recurs": EffectRetryRecurs,
"effect-sleep": EffectSleep,
"effect-succeed": EffectSucceed,
"effect-sync": EffectSync,
"effect-timeout": EffectTimeout,
"effect-validate": EffectValidate,
"ref-make": RefMake,
"ref-update-and-get": RefUpdateAndGet,
}
import { defaultSpring } from "@/animations"
import { EffectLogo } from "@/components/feedback"
import { NavigationSidebar } from "@/components/layout/NavigationSidebar"
import { PageHeader } from "@/components/layout/PageHeader"
import { QuickOpen } from "@/components/ui"
import type { AppItem } from "@/lib/example-types"
import { appItems, createExampleId } from "@/shared/appItems"
import { taskSounds } from "@/sounds/TaskSounds"
// Helper function to get item section
function getItemSection(item: AppItem): string {
return item.metadata.section
}
function AppContentInner() {
const [isMuted, setIsMuted] = useState(false)
// Update sound system when mute changes
useEffect(() => {
taskSounds.setMuted(isMuted)
}, [isMuted])
const [currentExampleId, setCurrentExampleId] = useState<string | undefined>()
// Handle example selection from sidebar
const handleExampleSelect = useCallback((id: string) => {
setCurrentExampleId(id)
// Scroll to the element
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" })
}
}, [])
// Prepare example metadata once for both navigation and quick-open
const exampleDisplayItems = useMemo(
() =>
appItems.map(item => ({
id: createExampleId(item.metadata.name, item.metadata.variant),
name: item.metadata.name,
...(item.metadata.variant ? { variant: item.metadata.variant } : {}),
section: item.metadata.section,
})),
[],
)
const exampleIdSet = useMemo(
() => new Set(exampleDisplayItems.map(example => example.id)),
[exampleDisplayItems],
)
const pathname = usePathname()
useEffect(() => {
if (!pathname) return
const segments = pathname.split("/").filter(Boolean)
const rawTarget = segments.at(-1)
if (!rawTarget) {
setCurrentExampleId(undefined)
return
}
const targetId = decodeURIComponent(rawTarget)
if (!exampleIdSet.has(targetId)) return
window.requestAnimationFrame(() => {
// RequestAnimationFrame ensures the DOM is ready before attempting to scroll.
handleExampleSelect(targetId)
})
}, [pathname, exampleIdSet, handleExampleSelect])
return (
<div className="min-h-screen font-mono relative overflow-hidden">
{/* Command-K quick-open modal */}
<QuickOpen items={exampleDisplayItems} onSelect={handleExampleSelect} />
<div className="max-w-screen-l mx-auto relative">
{/* Navigation Sidebar */}
<NavigationSidebar
examples={exampleDisplayItems}
currentExample={currentExampleId || undefined}
onExampleSelect={handleExampleSelect}
/>
<div className="xl:ml-64 p-4 sm:p-8">
<div className="w-full flex flex-col items-center max-w-screen-md mx-auto relative z-10">
<PageHeader isMuted={isMuted} onMuteToggle={() => setIsMuted(!isMuted)} />
{/* Introduction section */}
<div className="w-full max-w-screen-md mt-24 mb-12 p-8 border border-neutral-700/50 rounded-2xl shadow-2xl bg-gradient-to-br from-neutral-900/80 to-neutral-900/40 backdrop-blur-sm font-mono text-neutral-300 text-lg relative overflow-hidden">
<div className="relative z-10">
<p className="leading-relaxed text-left text-xl font-light">
Here are some interactive examples of TypeScript's beautiful{" "}
<EffectLogo className="inline-block h-4 relative top-[-1px] pr-3 opacity-90" />
<a
href="https://effect.website"
target="_blank"
rel="noopener noreferrer"
className="font-extrabold cursor-pointer hover:text-white transition-all duration-300 tracking-wider inline-block"
>
Effect
</a>{" "}
library. Tap the following effects to{" "}
<PlayIcon size={16} className="inline mr-2" weight="bold" />
<span className="font-bold ">run</span>,{" "}
<StopIcon size={16} className="inline mr-2" weight="bold" />
<span className="font-bold ">interrupt</span>, or{" "}
<ArrowClockwiseIcon size={16} className="inline mr-2" weight="bold" />
<span className="font-bold ">reset</span> them.
</p>
</div>
</div>
{/* Multiple effect examples and callouts */}
<div className="w-full max-w-screen-md flex flex-col items-center gap-y-8 sm:gap-y-12">
{appItems.map((item, index) => {
const prevItem: AppItem | undefined = index > 0 ? appItems[index - 1] : undefined
const showSectionHeader =
index === 0 ||
(prevItem !== undefined && getItemSection(prevItem) !== getItemSection(item))
return (
<Fragment key={index}>
{showSectionHeader && (
<div className="w-full mt-16 mb-0">
<h2 className="text-lg sm:text-2xl font-mono text-neutral-300 tracking-wider font-bold relative">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 ">
<HashStraightIcon
weight="bold"
size={20}
className="text-neutral-400"
/>
</div>
<span className="text-neutral-400">
{getItemSection(item).toUpperCase()}
</span>
</div>
</h2>
</div>
)}
<div
className="w-full relative"
id={createExampleId(item.metadata.name, item.metadata.variant)}
>
{(() => {
const Component = exampleComponentById[item.metadata.id]
if (!Component) return null
return (
<Component
metadata={item.metadata}
index={index}
exampleId={createExampleId(item.metadata.name, item.metadata.variant)}
/>
)
})()}
</div>
</Fragment>
)
})}
</div>
{/* Footer */}
<footer className="w-full max-w-screen-md mt-40 mb-12 flex items-center justify-between text-xs sm:text-base">
{/* Left side */}
<div className="text-neutral-400 font-bold tracking-wide flex items-center gap-1.5 sm:gap-2">
EFFECT OR
<SkullIcon size={16} weight="fill" className="text-neutral-400 sm:hidden" />
<SkullIcon
size={19}
weight="fill"
className="text-neutral-400 hidden sm:block ml-[3px]"
/>
</div>
{/* Right side */}
<a
href="https://twitter.com/kitlangton"
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 hover:text-neutral-200 transition-all duration-300 font-bold tracking-wide group flex items-center gap-1.5 sm:gap-2"
>
<HeartIcon
size={14}
weight="fill"
className="text-red-500 group-hover:text-red-400 transition-transform duration-300 group-hover:scale-110 sm:hidden"
/>
<HeartIcon
size={18}
weight="fill"
className="text-red-500 group-hover:text-red-400 transition-transform duration-300 group-hover:scale-110 hidden sm:block"
/>
KIT
</a>
</footer>
</div>
</div>
</div>
</div>
)
}
export function AppContent() {
return (
<MotionConfig transition={defaultSpring}>
<AppContentInner />
</MotionConfig>
)
}
================================================
FILE: src/VisualEffect.test.ts
================================================
import { Effect } from "effect"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { VisualEffect } from "./VisualEffect"
describe("VisualEffect State Transitions", () => {
let task: VisualEffect<string, string>
let stateChanges: Array<{ from: string; to: string; timestamp: number }> = []
beforeEach(() => {
stateChanges = []
// Don't use fake timers by default - many tests need real async behavior
})
afterEach(() => {
vi.useRealTimers()
task?.reset()
})
const createTask = (effect: Effect.Effect<string, string>) => {
const visualTask = new VisualEffect("test", effect)
// Track all state changes
visualTask.subscribe(() => {
const currentState = visualTask.state
const timestamp = Date.now()
if (stateChanges.length > 0) {
const lastState = stateChanges[stateChanges.length - 1]
stateChanges.push({
from: lastState?.to ?? "initial",
to: currentState.type,
timestamp,
})
} else {
stateChanges.push({
from: "initial",
to: currentState.type,
timestamp,
})
}
})
return visualTask
}
describe("Basic State Transitions", () => {
it("should start in idle state", () => {
task = createTask(Effect.succeed("test"))
expect(task.state.type).toBe("idle")
})
it("should transition idle -> running -> completed for successful effect", async () => {
// Use an effect that takes time so we can observe the running state
task = createTask(Effect.sleep(50).pipe(Effect.map(() => "success")))
const promise = Effect.runPromise(task.effect)
// Should be running now
expect(task.state.type).toBe("running")
const result = await promise
expect(result).toBe("success")
expect(task.state.type).toBe("completed")
if (task.state.type === "completed") {
expect(task.state.result).toBe("success")
}
})
it("should transition idle -> running -> failed for failing effect", async () => {
// Use an effect that takes time before failing
task = createTask(Effect.sleep(50).pipe(Effect.flatMap(() => Effect.fail("error"))))
try {
await Effect.runPromise(task.effect)
} catch (error: unknown) {
if (error instanceof Error) {
expect(error.message).toBe("error")
} else {
throw error
}
}
expect(task.state.type).toBe("failed")
if (task.state.type === "failed") {
expect(task.state.error).toBe("error")
}
})
it("should transition to interrupted when effect is interrupted", async () => {
task = createTask(Effect.sleep(1000).pipe(Effect.map(() => "never")))
const runPromise = task.run()
expect(task.state.type).toBe("running")
// Interrupt the task
task.interrupt()
try {
await runPromise
} catch {
// Expected to throw
}
expect(task.state.type).toBe("interrupted")
})
})
describe("Reset Behavior", () => {
it("should reset from completed to idle", async () => {
task = createTask(Effect.succeed("done"))
await Effect.runPromise(task.effect)
expect(task.state.type).toBe("completed")
task.reset()
expect(task.state.type).toBe("idle")
})
it("should reset from failed to idle", async () => {
task = createTask(Effect.fail("error"))
try {
await Effect.runPromise(task.effect)
} catch {
// Expected to throw
}
expect(task.state.type).toBe("failed")
task.reset()
expect(task.state.type).toBe("idle")
})
it("should reset from running to idle (interrupt case)", async () => {
task = createTask(Effect.sleep(1000).pipe(Effect.map(() => "slow")))
const promise = Effect.runPromise(task.effect)
expect(task.state.type).toBe("running")
task.reset()
expect(task.state.type).toBe("idle")
try {
await promise
} catch {
// Expected to throw due to interruption
}
// Should stay idle after interruption completes
expect(task.state.type).toBe("idle")
})
it("should prevent interrupted state during reset", async () => {
task = createTask(Effect.sleep(1000).pipe(Effect.map(() => "slow")))
Effect.runPromise(task.effect)
expect(task.state.type).toBe("running")
// Reset should prevent "interrupted" state
task.reset()
// Even if async interruption happens, should stay idle
await vi.waitFor(() => {
expect(task.state.type).toBe("idle")
})
// Verify no interrupted state in history
const hasInterrupted = stateChanges.some(change => change.to === "interrupted")
expect(hasInterrupted).toBe(false)
})
})
describe("Effect Caching", () => {
it("should return cached result for completed effects", async () => {
let executionCount = 0
const countingEffect = Effect.sync(() => {
executionCount++
return `execution-${executionCount}`
})
task = createTask(countingEffect)
// First execution
const result1 = await Effect.runPromise(task.effect)
expect(result1).toBe("execution-1")
expect(executionCount).toBe(1)
// Second call should return cached result
const result2 = await Effect.runPromise(task.effect)
expect(result2).toBe("execution-1") // Same result
expect(executionCount).toBe(1) // No additional execution
})
it("should re-execute after reset", async () => {
let executionCount = 0
const countingEffect = Effect.sync(() => {
executionCount++
return `execution-${executionCount}`
})
task = createTask(countingEffect)
// First execution
await Effect.runPromise(task.effect)
expect(executionCount).toBe(1)
// Reset and execute again
task.reset()
const result2 = await Effect.runPromise(task.effect)
expect(result2).toBe("execution-2")
expect(executionCount).toBe(2)
})
})
describe("Parent-Child Relationships", () => {
it("should register child effects and reset them when parent is reset", async () => {
const childStates: Array<string> = []
// Create a child effect that tracks its state changes
const childEffect = Effect.gen(function* () {
const child = createTask(Effect.sleep(1000).pipe(Effect.map(() => "child-done")))
// Track child state changes
child.subscribe(() => {
childStates.push(child.state.type)
})
// Run the child as part of the parent's effect
// This will make it register with the parent via VisualEffectService
const childResult = yield* child.effect
return childResult
})
const parent = createTask(
Effect.gen(function* () {
console.log("Parent starting")
const result = yield* childEffect
console.log("Parent finishing")
return result
}),
)
// Start the parent
const parentPromise = parent.run()
expect(parent.state.type).toBe("running")
// Wait for the child to enter running state
await vi.waitFor(() => {
expect(childStates).toContain("running")
})
// Reset the parent - this should cascade to children
parent.reset()
expect(parent.state.type).toBe("idle")
try {
await parentPromise
} catch {
// Expected to throw due to interruption
}
// Child should have been reset too (transitioned to idle)
expect(childStates).toContain("running")
expect(childStates).toContain("idle")
// Verify child ended up in idle state
const finalChildState = childStates[childStates.length - 1]
expect(finalChildState).toBe("idle")
})
it("should interrupt child effects when parent is interrupted", async () => {
const childStates: Array<string> = []
// Create a child effect that tracks its state changes
const childEffect = Effect.gen(function* () {
const child = createTask(Effect.sleep(1000).pipe(Effect.map(() => "child-done")))
// Track child state changes
child.subscribe(() => {
childStates.push(child.state.type)
})
// Run the child as part of the parent's effect
// This ensures it gets registered with the parent
return yield* child.effect
})
const parent = createTask(childEffect)
// Start the parent
const parentPromise = parent.run()
expect(parent.state.type).toBe("running")
// Wait for child running
await vi.waitFor(() => {
expect(childStates).toContain("running")
})
// Interrupt the parent - this should cascade to children
parent.interrupt()
expect(parent.state.type).toBe("interrupted")
try {
await parentPromise
} catch {
// Expected to throw due to interruption
}
// Child should have been interrupted too
expect(childStates).toContain("running")
expect(childStates).toContain("interrupted")
})
})
})
================================================
FILE: src/VisualEffect.ts
================================================
"use client"
import { Context, Effect, Fiber, Option } from "effect"
import { useSyncExternalStore } from "react"
import { taskSounds } from "./sounds/TaskSounds"
export type EffectState<A, E> =
| { type: "idle" }
| { type: "running" }
| { type: "completed"; result: A }
| { type: "failed"; error: E }
| { type: "interrupted" }
| { type: "death"; error: unknown }
// Pattern matching helper for EffectState (internal use only)
const matchEffectState = <A, E, T>(
state: EffectState<A, E>,
cases: {
idle: () => T
running: () => T
completed: (result: A) => T
failed: (error: E) => T
interrupted: () => T
death: (error: unknown) => T
},
): T => {
switch (state.type) {
case "idle":
return cases.idle()
case "running":
return cases.running()
case "completed":
return cases.completed(state.result)
case "failed":
return cases.failed(state.error)
case "interrupted":
return cases.interrupted()
case "death":
return cases.death(state.error)
}
}
export interface Notification {
id: string
message: string
timestamp: number
duration?: number // auto-dismiss after this many ms
icon?: string // emoji or icon
}
// Valid state transitions for the state machine
const VALID_TRANSITIONS: Record<string, Set<string>> = {
idle: new Set(["running", "idle"]),
running: new Set(["completed", "failed", "interrupted", "death", "idle", "running"]),
completed: new Set(["idle", "running", "completed"]),
failed: new Set(["failed", "idle", "running"]),
interrupted: new Set(["interrupted", "idle", "running"]),
death: new Set(["death", "idle", "running"]),
}
// Service interface for parent-child VisualEffect communication
export interface VisualEffectService {
readonly addChild: (child: VisualEffect<unknown, unknown>) => Effect.Effect<void>
readonly notify: (
message: string,
options?: { duration?: number; icon?: string },
) => Effect.Effect<void>
}
const VisualEffectService = Context.GenericTag<VisualEffectService>("VisualEffectService")
// Service implementation
class VisualEffectServiceImpl implements VisualEffectService {
constructor(private parent: VisualEffect<unknown, unknown>) {}
addChild = (child: VisualEffect<unknown, unknown>): Effect.Effect<void> =>
Effect.sync(() => {
this.parent.addChildEffect(child)
})
notify = (message: string, options?: { duration?: number; icon?: string }): Effect.Effect<void> =>
Effect.sync(() => {
this.parent.notify(message, options)
})
}
export class VisualEffect<A, E = never> {
private listeners = new Set<() => void>()
private notificationListeners = new Set<() => void>()
private currentNotification: Notification | null = null
private fiber: Fiber.RuntimeFiber<A, E> | null = null
private timeouts = new Set<ReturnType<typeof setTimeout>>()
private isResetting = false
private children = new Set<VisualEffect<unknown, unknown>>()
state: EffectState<A, E> = { type: "idle" }
addChildEffect(child: VisualEffect<unknown, unknown>): void {
this.children.add(child)
}
startTime: number | null = null
endTime: number | null = null
constructor(
public name: string,
private _effect: Effect.Effect<A, E>,
public showTimer: boolean = false,
) {}
// The effect property returns an Effect that updates this effect's state when run
get effect(): Effect.Effect<A, E> {
// Quick return for terminal states
const quickReturn = matchEffectState(this.state, {
idle: () => null,
running: () => null,
completed: result => Effect.succeed(result),
failed: error => Effect.fail(error) as Effect.Effect<A, E>,
interrupted: () => null,
death: error => Effect.die(error),
})
if (quickReturn) return quickReturn
// Create the effect
return Effect.gen(
function* (this: VisualEffect<A, E>) {
// Register with parent service if available
const maybeParentService = yield* Effect.serviceOption(VisualEffectService)
yield* Option.match(maybeParentService, {
onNone: () => Effect.void,
onSome: service => service.addChild(this),
})
// Mark as running
this.setState({ type: "running" })
// Execute the wrapped effect with appropriate service provided to all nested effects
const effectWithRootService = this._effect.pipe(
Effect.provideService(VisualEffectService, new VisualEffectServiceImpl(this)),
)
const wrappedEffect = Option.isSome(maybeParentService)
? this._effect
: effectWithRootService
return yield* wrappedEffect
}.bind(this),
).pipe(
// Clear notifications on any non-success exit
Effect.tapErrorCause(() => Effect.sync(() => this.clearNotifications())),
// Handle success
Effect.tap(result =>
Effect.sync(() => {
this.setState({ type: "completed", result })
}),
),
// Handle errors
Effect.tapError((error: E) =>
Effect.sync(() => {
this.setState({ type: "failed", error })
}),
),
// Handle interruption
Effect.onInterrupt(() =>
Effect.sync(() => {
if (!this.isResetting) {
this.setState({ type: "interrupted" })
}
}),
),
// Handle defects
Effect.tapDefect((defect: unknown) =>
Effect.sync(() => {
if (process.env.NODE_ENV === "development") {
console.error(`Effect "${this.name}" died with defect:`, defect)
}
this.setState({ type: "death", error: defect })
}),
),
) as Effect.Effect<A, E>
}
// Observable pattern methods
subscribe(listener: () => void) {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}
subscribeToNotifications(listener: () => void) {
this.notificationListeners.add(listener)
return () => {
this.notificationListeners.delete(listener)
}
}
notify(message: string, options?: { duration?: number; icon?: string }): void {
// Clear any existing notification and its timeout
this.clearNotifications()
const notification: Notification = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
message,
timestamp: Date.now(),
duration: options?.duration ?? 2000, // default 2 seconds
...(options?.icon && { icon: options.icon }),
}
this.currentNotification = notification
this.notifyNotificationListeners()
// Auto-remove after duration
if (notification.duration) {
const timeoutId = setTimeout(() => {
this.clearNotifications()
}, notification.duration)
this.timeouts.add(timeoutId)
}
}
getCurrentNotification(): Notification | null {
return this.currentNotification
}
private clearNotifications(): void {
this.currentNotification = null
this.clearTimeouts()
this.notifyNotificationListeners()
}
private notifyStateListeners() {
this.listeners.forEach(listener => {
listener()
})
}
private notifyNotificationListeners() {
this.notificationListeners.forEach(listener => {
listener()
})
}
private setState(newState: EffectState<A, E>) {
if (this.isResetting) return
const previousState = this.state
const validTransitions = VALID_TRANSITIONS[previousState.type]
if (!validTransitions?.has(newState.type)) {
if (process.env.NODE_ENV === "development") {
console.warn(
`Invalid state transition from ${previousState.type} to ${newState.type} for task ${this.name}`,
)
}
return
}
this.state = newState
// Track timing
if (this.showTimer) {
if (newState.type === "running" && previousState.type !== "running") {
this.startTime = Date.now()
this.endTime = null
} else if (previousState.type === "running" && newState.type !== "running") {
this.endTime = Date.now()
}
}
// Trigger sounds
if (previousState.type !== newState.type) {
matchEffectState(newState, {
idle: () => {},
running: () => taskSounds.playRunning().catch(() => {}),
completed: () => taskSounds.playSuccess().catch(() => {}),
failed: () => taskSounds.playFailure().catch(() => {}),
interrupted: () => taskSounds.playInterrupted().catch(() => {}),
death: () => taskSounds.playDeath().catch(() => {}),
})
}
this.notifyStateListeners()
}
private clearTimeouts() {
this.timeouts.forEach(clearTimeout)
this.timeouts.clear()
}
reset() {
this.isResetting = true
try {
// Reset all children first so their state transitions obey the reset flag
this.children.forEach(child => {
child.reset()
})
// Clear the children collection since they're no longer relevant
this.children.clear()
// Interrupt our own fiber if it's still running
if (this.fiber) {
Effect.runFork(Fiber.interrupt(this.fiber))
this.fiber = null
}
// Clean up any scheduled work / caches
this.clearTimeouts()
this.clearNotifications() // Clear notifications on reset
this.startTime = null
this.endTime = null
} finally {
// Allow subsequent state transitions
this.isResetting = false
}
// Now that the reset flag is cleared, transition ourselves to idle
this.setState({ type: "idle" })
}
async run() {
try {
this.fiber = Effect.runFork(this.effect)
await Effect.runPromise(Fiber.await(this.fiber))
} catch {
// Error handling is done within the effect
} finally {
this.fiber = null
}
}
interrupt() {
if (this.state.type === "running") {
const fiberToInterrupt = this.fiber
this.fiber = null
// Optimistically mark as interrupted so observers update immediately.
// The onInterrupt handler inside the effect will confirm this later.
this.setState({ type: "interrupted" })
if (fiberToInterrupt) {
Effect.runFork(Fiber.interrupt(fiberToInterrupt))
}
}
}
}
// Utility function for effects to notify their parent
export const notify = (
message: string,
options?: { duration?: number; icon?: string },
): Effect.Effect<void, never> =>
Effect.serviceOption(VisualEffectService).pipe(
Effect.flatMap(option =>
Option.isSome(option) ? option.value.notify(message, options) : Effect.void,
),
)
// Granular React hooks for better performance
// Subscribe only to state changes
export function useVisualEffectState<A, E>(effect: VisualEffect<A, E>) {
return useSyncExternalStore(effect.subscribe.bind(effect), () => effect.state)
}
// Subscribe only to notification changes
export function useVisualEffectNotification<A, E>(effect: VisualEffect<A, E>) {
return useSyncExternalStore(effect.subscribeToNotifications.bind(effect), () =>
effect.getCurrentNotification(),
)
}
// Subscribe for re-renders only (no return value)
export function useVisualEffectSubscription<A, E>(effect: VisualEffect<A, E>) {
useSyncExternalStore(effect.subscribe.bind(effect), () => effect.state)
}
// Factory function - compatible with V1 signature
export const visualEffect = <A, E = never>(
name: string,
effect: Effect.Effect<A, E>,
showTimer: boolean = false,
) => new VisualEffect(name, effect, showTimer)
================================================
FILE: src/VisualRef.ts
================================================
"use client"
import { Effect, Ref } from "effect"
import { useMemo, useSyncExternalStore } from "react"
import { taskSounds } from "./sounds/TaskSounds"
export class VisualRef<A> {
private listeners = new Set<() => void>()
private _ref: Ref.Ref<A> | null = null
private _currentValue: A
private _justChanged = false
private animationTimeoutId: ReturnType<typeof setTimeout> | null = null
constructor(
public name: string,
private initialValue: A,
) {
this._currentValue = initialValue
}
get ref(): Effect.Effect<Ref.Ref<A>, never> {
if (this._ref) return Effect.succeed(this._ref)
return Ref.make(this.initialValue).pipe(
Effect.tap(ref =>
Effect.sync(() => {
this._ref = ref
}),
),
)
}
get value(): A {
return this._currentValue
}
get justChanged(): boolean {
return this._justChanged
}
updateValue(newValue: A): void {
if (this._currentValue === newValue) return
// Play sound
taskSounds.playRefUpdate().catch(() => {})
// Update state
this._currentValue = newValue
this._justChanged = true
// Clear existing timeout
if (this.animationTimeoutId) {
clearTimeout(this.animationTimeoutId)
}
// Schedule animation cleanup
this.animationTimeoutId = setTimeout(() => {
this._justChanged = false
this.notify()
this.animationTimeoutId = null
}, 50)
this.notify()
}
updateAndGet(updateFn: (current: A) => A): Effect.Effect<A> {
return this.ref.pipe(
Effect.flatMap(ref => Ref.updateAndGet(ref, updateFn)),
Effect.tap(newValue => Effect.sync(() => this.updateValue(newValue))),
)
}
get(): Effect.Effect<A> {
return this.ref.pipe(Effect.flatMap(Ref.get))
}
subscribe(listener: () => void) {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
private notify() {
for (const listener of this.listeners) {
listener()
}
}
reset() {
// Clear animation timeout
if (this.animationTimeoutId) {
clearTimeout(this.animationTimeoutId)
this.animationTimeoutId = null
}
// Reset state
this._currentValue = this.initialValue
this._justChanged = false
this._ref = null
this.notify()
}
}
export const visualRef = <A>(name: string, initialValue: A) => new VisualRef(name, initialValue)
export function useVisualRef<A>(ref: VisualRef<A>) {
const subscribe = useMemo(() => (listener: () => void) => ref.subscribe(listener), [ref])
const getSnapshot = useMemo(() => {
// Cache to avoid creating new objects on every read
let cache = {
value: ref.value,
justChanged: ref.justChanged,
}
return () => {
const currentValue = ref.value
const currentFlag = ref.justChanged
if (cache.value !== currentValue || cache.justChanged !== currentFlag) {
cache = { value: currentValue, justChanged: currentFlag }
}
return cache
}
}, [ref])
const snapshot = useSyncExternalStore(subscribe, getSnapshot)
return {
ref,
value: snapshot.value,
justChanged: snapshot.justChanged,
} as const
}
================================================
FILE: src/VisualScope.ts
================================================
import { taskSounds } from "./sounds/TaskSounds"
export type ScopeState = "idle" | "acquiring" | "active" | "releasing" | "released"
export type FinalizerState = "pending" | "running" | "completed"
export interface Finalizer {
id: string
name: string
timestamp: number
state: FinalizerState
}
export class VisualScope {
id: string
state: ScopeState = "idle"
finalizers: Array<Finalizer> = []
private subscribers: Set<() => void> = new Set()
constructor(id: string) {
this.id = id
}
subscribe(callback: () => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
private notify() {
this.subscribers.forEach(callback => {
callback()
})
}
setState(newState: ScopeState) {
if (this.state === newState) return
this.state = newState
this.notify()
}
addFinalizer(name: string): string {
const id = `finalizer-${name}`
const finalizer: Finalizer = {
id,
name,
timestamp: Date.now(),
state: "pending",
}
this.finalizers.push(finalizer)
taskSounds.playFinalizerCreated()
this.notify()
return id
}
async runFinalizers() {
this.setState("releasing")
// Run finalizers in reverse order (LIFO)
const finalizersToRun = [...this.finalizers].reverse()
for (const finalizer of finalizersToRun) {
// Abort if scope was reset while releasing
if (this.state !== "releasing") {
return
}
finalizer.state = "running"
taskSounds.playFinalizerRunning()
this.notify()
// Simulate finalizer execution
await new Promise(resolve => setTimeout(resolve, 800))
// If scope was reset during the simulated execution, abort further processing
if (this.state !== "releasing") {
return
}
finalizer.state = "completed"
taskSounds.playFinalizerCompleted()
this.notify()
}
// Only mark as released if we weren't reset in the meantime
if (this.state === "releasing") {
this.setState("released")
}
}
reset() {
this.state = "idle"
this.finalizers = []
this.notify()
}
}
================================================
FILE: src/animations.ts
================================================
// Animation configuration for consistent motion design across components
// Default spring for MotionConfig wrapper
export const defaultSpring = {
type: "spring" as const,
// Critical damping occurs when damping ≈ 2 * sqrt(stiffness * mass).
// With mass = 1, choose stiffness that feels snappy but natural and
// compute damping accordingly.
mass: 1,
stiffness: 200,
damping: 2 * Math.sqrt(200), // ≈ 28.28
// No bounce/overshoot for critically damped motion
bounce: 0,
}
// Spring presets
export const springs = {
// Main spring for general animations
default: {
type: "spring" as const,
stiffness: 180,
damping: 25,
mass: 0.8,
},
// Bouncy spring for completion animations
bouncy: {
type: "spring" as const,
bounce: 0.3,
visualDuration: 0.5,
},
// Node width animation with custom bounce
nodeWidth: {
type: "spring" as const,
stiffness: 180,
damping: 25,
mass: 0.8,
visualDuration: 0.6,
bounce: 0.3,
},
// Content scale animation for completion
contentScale: {
type: "spring" as const,
bounce: 0.3,
visualDuration: 0.5,
stiffness: 260,
damping: 18,
},
// Failure bubble animation
failureBubble: {
type: "spring" as const,
visualDuration: 0.2,
delay: 0.05,
bounce: 0.3,
},
}
// Shake animation constants
export const shake = {
// Running state jitter
running: {
angleRange: 4,
angleBase: 0.5,
offsetRange: 1.5,
offsetBase: 0.5,
offsetYRange: 0.6,
offsetYBase: 0.1,
durationMin: 0.1,
durationMax: 0.2,
},
// Failure/death shake - more intense
failure: {
intensity: 8,
duration: 0.08,
count: 6,
rotationRange: 8,
returnDuration: 0.3,
},
// Failure bubble shake - gentler
bubble: {
intensity: 4,
duration: 0.08,
count: 4,
rotationRange: 4,
yOffset: -5,
returnDuration: 0.3,
delay: 100,
},
}
// Animation durations and timing
export const timing = {
// Border pulsing
borderPulse: {
duration: 1.5,
values: [1, 0.3, 1],
},
// Glow pulsing
glowPulse: {
duration: 0.5,
values: [1, 5, 1],
},
// Flash animation
flash: {
duration: 1.0,
ease: "linear" as const,
},
// Smooth exit animations
exit: {
duration: 0.3,
ease: [0.4, 0, 0.6, 1] as const,
},
// Glitch effect timing
glitch: {
initialCount: 3,
initialDelayMin: 20,
initialDelayMax: 70,
pauseMin: 50,
pauseMax: 150,
subtleDelayMin: 300,
subtleDelayMax: 800,
},
}
// Color values for animations
export const colors = {
// Flash colors
flash: "rgba(255, 255, 255, 0.8)",
// Border colors
border: {
default: "rgba(255, 255, 255, 0.1)",
death: "rgba(220, 38, 38, 0.4)",
},
// Failure bubble colors
failureBubble: {
background: "rgba(239, 68, 68, 0.95)",
text: "text-red-50",
shadow: "0 0px 16px rgba(0, 0, 0, 0.5)",
},
// Glow effects
glow: {
death: "rgba(220, 38, 38, 0.8)",
running: "rgba(100, 200, 255, 0.2)",
},
}
// Transform and filter values
export const effects = {
// Death filter effects
death: {
contrast: 1.2,
brightness: 0.8,
},
// Glitch intensity ranges
glitch: {
scaleRange: 0.2,
glowMin: 3,
glowMax: 7,
intensePulseMax: 10,
},
}
================================================
FILE: src/components/CodeBlock.tsx
================================================
import type { MotionStyle } from "motion/react"
import { AnimatePresence, motion } from "motion/react"
import type { Language, RenderProps, Token } from "prism-react-renderer"
import { Highlight, themes } from "prism-react-renderer"
import type React from "react"
import { useEffect, useMemo, useRef } from "react"
const { oneDark } = themes
interface CodeBlockProps {
code: string
language?: Language
/**
* Line numbers (1-based) that should be visually highlighted.
*/
activeLines?: Array<number>
/**
* Called when the user hovers a line. null means hover left the block.
*/
onLineHover?: (lineNo: number | null) => void
/**
* Optional style overrides for the <pre> element.
*/
style?: React.CSSProperties
}
export const CodeBlock: React.FC<CodeBlockProps> = ({
activeLines = [],
code,
language = "typescript",
onLineHover,
style,
}) => {
// Ensure active lines are unique for comparison in effect deps
const active = Array.from(new Set(activeLines))
// Store previous lines to compare for changes
const prevLinesRef = useRef<Array<string>>([])
const isInitialRender = useRef(true)
// Split current code into lines for comparison
const currentLines = useMemo(() => code.trim().split("\n"), [code])
// Determine stable vs new/removed lines by content
const lineStates = useMemo(() => {
const prev = prevLinesRef.current
const prevSet = new Set(prev)
const currentSet = new Set(currentLines)
// Don't animate on initial render
if (isInitialRender.current) {
prevLinesRef.current = [...currentLines]
isInitialRender.current = false
return { stable: currentSet, new: new Set(), removed: new Set() }
}
const stableLines = new Set()
const newLines = new Set()
const removedLines = new Set()
// Find stable lines (exist in both)
for (const line of currentLines) {
if (prevSet.has(line)) {
stableLines.add(line)
} else {
newLines.add(line)
}
}
// Find removed lines (existed before but not now)
for (const line of prev) {
if (!currentSet.has(line)) {
removedLines.add(line)
}
}
// Update the ref for next comparison
prevLinesRef.current = [...currentLines]
return { stable: stableLines, new: newLines, removed: removedLines }
}, [currentLines])
useEffect(() => {
if (active.length === 0) return
// Scroll the first active line into view smoothly
const selector = `[data-line-no="${active[0]}"]`
const el = document.querySelector(selector)
el?.scrollIntoView({ behavior: "smooth", block: "center" })
}, [active])
return (
<motion.div
transition={{
type: "spring",
visualDuration: 0.1,
bounce: 0,
}}
style={{ overflow: "hidden" }}
>
<Highlight theme={oneDark} code={code.trim()} language={language}>
{(highlightProps: RenderProps) => {
const {
className,
getLineProps,
getTokenProps,
style: defaultStyle,
tokens,
} = highlightProps
return (
<pre
className={className}
style={{
...defaultStyle,
margin: 0,
borderRadius: 0,
padding: 0,
fontFamily: "Consolas, Monaco, 'Courier New', monospace",
lineHeight: 1.6,
backgroundColor: "transparent",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
wordWrap: "break-word",
maxWidth: "100%",
width: "100%",
...style,
}}
>
<AnimatePresence mode="popLayout" initial={false}>
{tokens.map((lineTokens: Token[], lineIndex: number) => {
const lineNo = lineIndex + 1
const isActive = active.includes(lineNo)
const lineContent = currentLines[lineIndex] ?? ""
// const isNewLine = lineStates.new.has(lineContent);
const isNewLine = lineStates.new.has(lineContent)
const lineProps = getLineProps({
line: lineTokens,
key: lineIndex,
})
const lineStyle: React.CSSProperties = {
display: "block",
paddingLeft: 0,
overflow: "hidden",
...(isActive ? { background: "rgba(56, 189, 248, 0.15)" } : {}),
...(lineProps.style ?? {}),
}
const motionStyle = lineStyle as MotionStyle
return (
<motion.div
key={`${lineIndex}-${lineContent}`}
className={lineProps.className}
style={motionStyle}
data-line-no={lineNo}
initial={isNewLine ? { opacity: 0, filter: "blur(6px)", height: 0 } : false}
animate={{
opacity: 1,
filter: "blur(0px)",
height: "auto",
}}
exit={{ opacity: 0, filter: "blur(6px)", height: 0 }}
transition={{
type: "spring",
visualDuration: 0.1,
bounce: 0,
}}
onMouseEnter={() => onLineHover?.(lineNo)}
onMouseLeave={() => onLineHover?.(null)}
>
{lineTokens.map((token: Token, tokenIndex: number) => {
const tokenProps = getTokenProps({
token,
key: tokenIndex,
})
// React keys must be passed directly rather than via spread props
const { key: _ignored, ...restTokenProps } = tokenProps
return <span key={`${lineNo}-${tokenIndex}`} {...restTokenProps} />
})}
</motion.div>
)
})}
</AnimatePresence>
</pre>
)
}}
</Highlight>
</motion.div>
)
}
================================================
FILE: src/components/HeaderView.tsx
================================================
"use client"
import {
ArrowCounterClockwiseIcon,
CheckIcon,
LinkIcon,
PlayIcon,
StarFourIcon,
StopIcon,
} from "@phosphor-icons/react"
import { AnimatePresence, motion } from "motion/react"
import { memo, useCallback, useEffect, useState } from "react"
import { GLOW_COLORS, TASK_COLORS } from "@/constants/colors"
import { useOptionKey } from "@/hooks/useOptionKey"
import { taskSounds } from "@/sounds/TaskSounds"
import { useVisualEffectSubscription, type VisualEffect } from "@/VisualEffect"
import type { VisualRef } from "@/VisualRef"
interface HeaderViewProps<A, E> {
effect: VisualEffect<A, E>
name: string
variant?: string
description?: React.ReactNode
refs?: Array<VisualRef<unknown>>
exampleId: string
}
function HeaderViewComponent({
description,
exampleId,
name,
refs = [],
effect: task,
variant,
}: HeaderViewProps<unknown, unknown>) {
const [isHovered, setIsHovered] = useState(false)
const [isPressed, setIsPressed] = useState(false)
const [showCheckmark, setShowCheckmark] = useState(false)
const [hasPlayedHoverSound, setHasPlayedHoverSound] = useState(false)
const isOptionPressed = useOptionKey()
useVisualEffectSubscription(task)
const { state } = task
const isRunning = state.type === "running"
const isCompleted = state.type === "completed"
const isFailed = state.type === "failed"
const isInterrupted = state.type === "interrupted"
const isDeath = state.type === "death"
const canReset = isCompleted || isFailed || isInterrupted || isDeath
const runWithDependencies = useCallback(async () => {
await task.run()
}, [task])
const resetWithDependencies = useCallback(() => {
task.reset()
// Also reset refs passed in
refs.forEach(refItem => {
refItem.reset()
})
// Play reset sound
taskSounds.playReset()
}, [task, refs])
const handleAction = useCallback(() => {
// If Option is pressed and we have an exampleId, copy link
if (isOptionPressed && exampleId) {
const url = `${window.location.origin}/${exampleId}`
navigator.clipboard.writeText(url).then(() => {
setShowCheckmark(true)
// Play copy success sound
taskSounds.playLinkCopied()
// Hide checkmark after 1.5 seconds
setTimeout(() => {
setShowCheckmark(false)
}, 1500)
})
return
}
const currentState = task.state
const running = currentState.type === "running"
const resettable =
currentState.type === "completed" ||
currentState.type === "failed" ||
currentState.type === "interrupted" ||
currentState.type === "death"
if (running) {
task.interrupt()
} else if (resettable) {
resetWithDependencies()
} else {
runWithDependencies()
}
}, [task, resetWithDependencies, runWithDependencies, isOptionPressed, exampleId])
const getIcon = () => {
// Show checkmark after copying
if (showCheckmark) {
return (
<motion.div
key="check"
initial={{ scale: 0, rotate: -180, filter: "blur(10px)" }}
animate={{ scale: 1, rotate: 0, filter: "blur(0px)" }}
exit={{ scale: 0, rotate: 180, filter: "blur(10px)" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<CheckIcon size={24} weight="bold" />
</motion.div>
)
}
// Show link icon when Option is pressed AND hovering
if (isOptionPressed && isHovered && exampleId) {
return (
<motion.div
key="link"
initial={{ scale: 0, rotate: -180, filter: "blur(10px)" }}
animate={{ scale: 1, rotate: 0, filter: "blur(0px)" }}
exit={{ scale: 0, rotate: 180, filter: "blur(10px)" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<LinkIcon size={24} weight="bold" />
</motion.div>
)
}
if (isHovered) {
if (isRunning) {
return (
<motion.div
key="stop"
initial={{ scale: 0, rotate: -180, filter: "blur(10px)" }}
animate={{ scale: 1, rotate: 0, filter: "blur(0px)" }}
exit={{ scale: 0, rotate: 180, filter: "blur(10px)" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<StopIcon size={24} weight="fill" />
</motion.div>
)
} else if (canReset) {
return (
<motion.div
key="reset"
initial={{ scale: 0, rotate: -180, filter: "blur(10px)" }}
animate={{ scale: 1, rotate: 0, filter: "blur(0px)" }}
exit={{ scale: 0, rotate: 180, filter: "blur(10px)" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<ArrowCounterClockwiseIcon size={24} weight="bold" />
</motion.div>
)
} else {
return (
<motion.div
key="play"
initial={{ scale: 0, rotate: -180, filter: "blur(10px)" }}
animate={{ scale: 1, rotate: 0, filter: "blur(0px)" }}
exit={{ scale: 0, rotate: 180, filter: "blur(10px)" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<PlayIcon size={24} weight="fill" />
</motion.div>
)
}
}
return (
<motion.div
key="star"
initial={{ scale: 0, filter: "blur(10px)" }}
animate={
isRunning
? { rotate: 360, scale: 1, filter: "blur(0px)" }
: { rotate: 0, scale: 1, filter: "blur(0px)" }
}
exit={{ scale: 0, filter: "blur(10px)" }}
transition={
isRunning
? {
rotate: {
duration: 1,
repeat: Infinity,
ease: "circInOut",
},
scale: { type: "spring", stiffness: 300, damping: 20 },
filter: { type: "spring", stiffness: 300, damping: 20 },
}
: {
type: "spring",
stiffness: 300,
damping: 20,
}
}
>
<StarFourIcon size={24} weight="fill" />
</motion.div>
)
}
// Play hover sound effect when Option is pressed and hovering
useEffect(() => {
if (isOptionPressed && isHovered && exampleId && !hasPlayedHoverSound) {
taskSounds.playLinkHover()
setHasPlayedHoverSound(true)
} else if (!isOptionPressed || !isHovered) {
setHasPlayedHoverSound(false)
}
}, [isOptionPressed, isHovered, exampleId, hasPlayedHoverSound])
return (
<motion.div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onClick={handleAction}
initial={{
borderColor: "rgba(255, 255, 255, 1)",
}}
animate={{
borderColor: "rgba(255, 255, 255, 0)",
}}
className="flex items-center gap-4 cursor-pointer rounded-lg p-3 -m-3 px-4 -mx-4"
>
<motion.div
animate={{
scale: isPressed ? 0.95 : isHovered ? 1.05 : 1,
background: showCheckmark
? "#4f46e5" // indigo-600 for success state
: isOptionPressed && isHovered && exampleId
? "#6366f1" // indigo-500 for link copy mode
: isRunning
? TASK_COLORS.running
: isInterrupted
? TASK_COLORS.interrupted
: isCompleted
? TASK_COLORS.success
: isFailed
? TASK_COLORS.error
: isDeath
? TASK_COLORS.death
: TASK_COLORS.idle,
}}
transition={{
scale: { type: "spring", stiffness: 300, damping: 20 },
background: { duration: 0.2, ease: "easeInOut" },
}}
className="w-10 h-10 rounded-md flex items-center justify-center text-white relative overflow-hidden"
>
<AnimatePresence mode="popLayout">{getIcon()}</AnimatePresence>
{isRunning && (
<motion.div
className="absolute -inset-0.5 -z-10"
style={{
background: `radial-gradient(circle, ${GLOW_COLORS.running} 0%, transparent 70%)`,
}}
animate={{
scale: [1, 1.3, 1],
opacity: [0.5, 0, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
)}
{/* Glow effect for link copy mode */}
{isOptionPressed && isHovered && exampleId && !showCheckmark && (
<motion.div
className="absolute -inset-0.5 -z-10"
style={{
background: `radial-gradient(circle, #6366f1 0%, transparent 70%)`,
}}
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.1, 0.3],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut",
}}
/>
)}
{/* Glow effect for success checkmark */}
{showCheckmark && (
<motion.div
className="absolute -inset-0.5 -z-10"
style={{
background: `radial-gradient(circle, #4f46e5 0%, transparent 70%)`,
}}
animate={{
scale: [1, 1.4, 1],
opacity: [0.6, 0, 0.6],
}}
transition={{
duration: 1,
repeat: Infinity,
ease: "easeInOut",
}}
/>
)}
</motion.div>
<div className="flex-1 flex flex-col">
<h2 className="text-xl font-semibold text-white flex items-baseline gap-2">
<span>{name}</span>
{/* <span className="text-neutral-500"> */}
{/* <CaretDoubleRightIcon size={16} weight="bold" /> */}
{/* </span> */}
{variant && <span className="font-medium text-neutral-500">{variant}</span>}
</h2>
{description && <p className="text-sm text-neutral-400">{description}</p>}
</div>
</motion.div>
)
}
export const HeaderView = memo(HeaderViewComponent) as typeof HeaderViewComponent
================================================
FILE: src/components/ScheduleTimeline.tsx
================================================
import { motion } from "motion/react"
import React, { useEffect, useRef, useState } from "react"
import type { VisualEffect } from "@/VisualEffect"
import { useVisualEffectState } from "@/VisualEffect"
// Timeline styling configuration
const TIMELINE_CONFIG = {
// Dimensions
height: 50,
lineThickness: 3,
dotSize: 12,
cursorWidth: 3,
cursorHeight: 30,
// Colors
colors: {
running: "bg-blue-500",
runningActive: "bg-blue-400", // Brighter for active segments
gap: "bg-neutral-500",
gapActive: "bg-neutral-500", // Brighter for active segments
cursor: "bg-white",
cursorInactive: "bg-neutral-600", // Darker cursor when stopped
backgroundLine: "bg-neutral-800", // Very dark gray background line
tickMark: "bg-neutral-800", // Subtle tick marks
},
// Raw color values for smooth animations
rawColors: {
runningActive: "var(--color-blue-400)",
runningInactive: "var(--color-blue-500)",
gapActive: "var(--color-neutral-400)",
gapInactive: "var(--color-neutral-600)",
backgroundLine: "var(--color-neutral-800)",
tickMark: "var(--color-neutral-800)",
cursorActive: "var(--color-white)",
cursorInactive: "var(--color-neutral-500)",
},
// Positioning
dotOffset: 6, // Half of dot size for centering
lineTopOffset: "50%", // Vertical center
dotTopOffset: "39%", // Slightly above center for visual balance
leftPadding: 80, // Padding from left edge to avoid fade zone
startOffset: 50, // Initial offset to make first dot visible
// Tick marks
tickMarkWidth: 1, // Width of tick marks
tickMarkSpacing: 50, // Spacing between tick marks in pixels
// Animation
animation: {
segmentDuration: 0.1,
cursorDuration: 0.05,
dotSpring: { type: "spring", visualDuration: 0.5, bounce: 0.4 },
activeToInactive: {
type: "spring",
visualDuration: 0.5,
bounce: 0.4,
},
},
} as const
export interface ScheduleTimelineProps {
baseEffect: VisualEffect<unknown, unknown>
repeatEffect: VisualEffect<unknown, unknown>
className?: string
pixelsPerSecond?: number
scrollThreshold?: number
}
interface TrailSegment {
id: string
startX: number
endX: number
type: "running" | "gap"
complete: boolean
startTime?: number // For calculating duration
endTime?: number
}
export function ScheduleTimeline({
baseEffect: baseTask,
className = "",
pixelsPerSecond = 100,
repeatEffect: repeatTask,
scrollThreshold = 0.8,
}: ScheduleTimelineProps) {
const containerRef = useRef<HTMLDivElement>(null)
const baseState = useVisualEffectState(baseTask)
const repeatState = useVisualEffectState(repeatTask)
const [isActive, setIsActive] = useState(false)
const [startTime, setStartTime] = useState<number | null>(null)
const [currentX, setCurrentX] = useState(0)
const [scrollOffset, setScrollOffset] = useState(0)
const [trailSegments, setTrailSegments] = useState<Array<TrailSegment>>([])
const [lastTaskState, setLastTaskState] = useState<string | null>(null)
const [isClearing, setIsClearing] = useState(false)
const [, setElapsedTime] = useState(0)
const [, setFinalElapsedTime] = useState<number | null>(null)
// Generate unique segment ID
const generateSegmentId = () => `segment-${Date.now()}-${Math.random()}`
// Format time function (same as TaskNode)
const formatTime = (ms: number) => {
return `${ms}ms`
}
// Handle base task state changes and timeline activation
useEffect(() => {
// Reset timeline when base task is reset to idle state
if (baseState.type === "idle" && (isActive || trailSegments.length > 0)) {
setIsClearing(true)
setTimeout(() => {
setIsActive(false)
setStartTime(null)
setLastTaskState(null)
setIsClearing(false)
setTrailSegments([])
setCurrentX(TIMELINE_CONFIG.startOffset)
setScrollOffset(0)
setElapsedTime(0)
setFinalElapsedTime(null)
}, 300)
return
}
// Handle base task state changes for trail coloring
if (isActive && lastTaskState !== baseState.type && startTime) {
if (lastTaskState === null && baseState.type === "running") {
setLastTaskState("running")
return
}
const now = Date.now()
const elapsed = now - startTime
const currentPosition = TIMELINE_CONFIG.startOffset + (elapsed / 1000) * pixelsPerSecond
setTrailSegments(prev => {
const updated = [...prev]
if (updated.length > 0) {
const lastIndex = updated.length - 1
const last = updated[lastIndex]
if (last) {
last.endX = currentPosition
last.complete = true
last.endTime = now
}
}
const segmentType = baseState.type === "running" ? "running" : "gap"
updated.push({
id: generateSegmentId(),
startX: currentPosition,
endX: currentPosition,
type: segmentType,
complete: false,
startTime: now,
})
return updated
})
setLastTaskState(baseState.type)
}
}, [baseState.type, isActive, lastTaskState, startTime, pixelsPerSecond, trailSegments.length])
// Handle repeat task state changes
useEffect(() => {
// Start timeline when repeat task starts running
if (repeatState.type === "running" && !isActive) {
setIsActive(true)
setStartTime(Date.now())
setCurrentX(TIMELINE_CONFIG.startOffset)
setScrollOffset(0)
setTrailSegments([])
setLastTaskState(null)
return
}
// Stop timeline animation when repeat task completes, fails, or is interrupted
if (
(repeatState.type === "completed" ||
repeatState.type === "failed" ||
repeatState.type === "interrupted") &&
isActive
) {
setTrailSegments(prev => {
const updated = [...prev]
if (updated.length > 0) {
const lastIndex = updated.length - 1
const last = updated[lastIndex]
if (last) {
last.complete = true
last.endTime = Date.now()
}
}
return updated
})
if (startTime) {
setFinalElapsedTime(Date.now() - startTime)
}
setIsActive(false)
return
}
// Reset timeline only when tasks are reset to idle state
if (repeatState.type === "idle" && (isActive || trailSegments.length > 0)) {
setIsClearing(true)
setTimeout(() => {
setIsActive(false)
setStartTime(null)
setLastTaskState(null)
setIsClearing(false)
setTrailSegments([])
setCurrentX(TIMELINE_CONFIG.startOffset)
setScrollOffset(0)
setElapsedTime(0)
setFinalElapsedTime(null)
}, 300)
return
}
}, [repeatState.type, isActive, startTime, trailSegments.length])
// Animation loop - just moves cursor and extends current segment
useEffect(() => {
if (!isActive || !startTime) return
let animationFrame: number
const animate = () => {
const now = Date.now()
const elapsed = now - startTime
// Calculate new cursor position
const newX = TIMELINE_CONFIG.startOffset + (elapsed / 1000) * pixelsPerSecond
// Update elapsed time
setElapsedTime(elapsed)
// Extend the current (last) segment to cursor position
setTrailSegments(prev => {
const updated = [...prev]
if (updated.length > 0) {
const lastIndex = updated.length - 1
const last = updated[lastIndex]
if (last) {
last.endX = newX
}
} else {
// First segment if none exist - only start when base task is running
if (baseState.type === "running") {
updated.push({
id: generateSegmentId(),
startX: TIMELINE_CONFIG.startOffset,
endX: newX,
type: "running",
complete: false,
startTime,
})
}
}
return updated
})
// Handle scrolling when cursor gets near the edge (simplified for now)
const timelineWidth = containerRef.current?.offsetWidth || 500
const scrollThresholdX = timelineWidth * scrollThreshold
if (newX > scrollThresholdX) {
setScrollOffset(newX - scrollThresholdX)
}
setCurrentX(newX)
animationFrame = requestAnimationFrame(animate)
}
animationFrame = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationFrame)
}
}, [isActive, startTime, baseState.type, pixelsPerSecond, scrollThreshold])
return (
<div className={`w-full ${className} relative`} ref={containerRef}>
{/* Timeline container */}
{/* Black gradient mask from left to right */}
{/* <div
className="absolute pointer-events-none z-20"
style={{
left: "-16px",
right: "0",
top: "0",
bottom: "0",
background:
"linear-gradient(to right, rgba(0,0,0,1) 0%, transparent 20px)",
}}
/> */}
<div
className="relative w-full overflow-hidden"
style={{
height: `${TIMELINE_CONFIG.height}px`,
}}
>
{/* Content with fade mask */}
<div className="relative w-full h-full">
{/* Background line */}
<div
className="absolute w-full"
style={{
height: `${TIMELINE_CONFIG.lineThickness}px`,
top: TIMELINE_CONFIG.lineTopOffset,
transform: "translateY(-50%)",
backgroundColor: TIMELINE_CONFIG.rawColors.backgroundLine,
}}
/>
{/* Tick marks */}
{(() => {
const containerWidth = containerRef.current?.offsetWidth || 1000
const totalWidth = containerWidth + scrollOffset + 500 // Add extra width for scrolling
const tickCount = Math.ceil(totalWidth / TIMELINE_CONFIG.tickMarkSpacing)
const ticks = []
for (let i = 1; i <= tickCount; i++) {
const x = i * TIMELINE_CONFIG.tickMarkSpacing - scrollOffset
// Only render ticks that are visible
if (
x >= -TIMELINE_CONFIG.tickMarkSpacing &&
x <= containerWidth + TIMELINE_CONFIG.tickMarkSpacing
) {
ticks.push(
<div
key={i}
className="absolute"
style={{
left: `${x}px`,
width: `${TIMELINE_CONFIG.tickMarkWidth}px`,
height: `${TIMELINE_CONFIG.height}px`,
top: "0",
backgroundColor: TIMELINE_CONFIG.rawColors.tickMark,
}}
/>,
)
}
}
return ticks
})()}
{/* Trail segments */}
{trailSegments.map(segment => {
const width = segment.endX - segment.startX
const left = segment.startX - scrollOffset - 16
if (segment.type === "running") {
const isActive = !segment.complete
return (
<div key={segment.id}>
{/* Blue line */}
<motion.div
className="absolute"
style={{
left: `${left}px`,
width: `${width}px`,
height: `${TIMELINE_CONFIG.lineThickness}px`,
top: TIMELINE_CONFIG.lineTopOffset,
transform: "translateY(-50%)",
}}
initial={{ width: 0 }}
animate={{
width: `${width}px`,
backgroundColor: isActive
? TIMELINE_CONFIG.rawColors.runningActive
: TIMELINE_CONFIG.rawColors.runningInactive,
opacity: isClearing ? 0 : 1,
}}
transition={{
width: {
duration: 0, // No delay for width animation
},
backgroundColor: TIMELINE_CONFIG.animation.activeToInactive,
opacity: {
duration: 0.3,
ease: "easeInOut",
},
}}
/>
{/* Start dot */}
<motion.div
className="absolute rounded-full z-12"
style={{
left: `${left - TIMELINE_CONFIG.dotOffset}px`,
width: `${TIMELINE_CONFIG.dotSize}px`,
height: `${TIMELINE_CONFIG.dotSize}px`,
top: TIMELINE_CONFIG.dotTopOffset,
transform: "translateY(-50%)",
}}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: 1,
opacity: isClearing ? 0 : 1,
backgroundColor: isActive
? TIMELINE_CONFIG.rawColors.runningActive
: TIMELINE_CONFIG.rawColors.runningInactive,
}}
transition={{
scale: {
...TIMELINE_CONFIG.animation.dotSpring,
},
opacity: isClearing
? {
duration: 0.3,
ease: "easeInOut",
}
: {
...TIMELINE_CONFIG.animation.dotSpring,
},
backgroundColor: TIMELINE_CONFIG.animation.activeToInactive,
}}
/>
{/* End dot (only if segment is complete) */}
{segment.complete && (
<motion.div
className="absolute rounded-full z-12"
style={{
left: `${left + width - TIMELINE_CONFIG.dotOffset}px`,
width: `${TIMELINE_CONFIG.dotSize}px`,
height: `${TIMELINE_CONFIG.dotSize}px`,
top: TIMELINE_CONFIG.dotTopOffset,
transform: "translateY(-50%)",
}}
initial={{
scale: 0,
opacity: 0,
backgroundColor: TIMELINE_CONFIG.rawColors.runningActive, // Start with active color
}}
animate={{
scale: 1,
opacity: isClearing ? 0 : 1,
backgroundColor: TIMELINE_CONFIG.rawColors.runningInactive, // Animate to inactive color
}}
transition={{
scale: {
...TIMELINE_CONFIG.animation.dotSpring,
},
opacity: isClearing
? {
duration: 0.3,
ease: "easeInOut",
}
: {
...TIMELINE_CONFIG.animation.dotSpring,
},
backgroundColor: TIMELINE_CONFIG.animation.activeToInactive,
borderColor: TIMELINE_CONFIG.animation.activeToInactive,
}}
/>
)}
</div>
)
} else {
const isActive = !segment.complete
// Calculate current duration - either completed or elapsed so far
let segmentDuration = null
if (segment.startTime) {
if (segment.endTime) {
// Completed segment
segmentDuration = segment.endTime - segment.startTime
} else if (isActive && startTime) {
// Active segment - show elapsed time
segmentDuration = Date.now() - segment.startTime
}
}
// Calculate clean positions without all the offset confusion
const segmentStartX = segment.startX - scrollOffset
const segmentEndX = segment.endX - scrollOffset
const segmentWidth = segmentEndX - segmentStartX
return (
<React.Fragment key={segment.id}>
{/* Gap line */}
<motion.div
className="absolute rounded-full"
style={{
left: `${segmentStartX - 16}px`, // Apply -16 offset only here
width: `${segmentWidth}px`,
height: `${TIMELINE_CONFIG.lineThickness}px`,
top: TIMELINE_CONFIG.lineTopOffset,
transform: "translateY(-50%)",
}}
initial={{ width: 0 }}
animate={{
width: `${segmentWidth}px`,
backgroundColor: isActive
? TIMELINE_CONFIG.rawColors.gapActive
: TIMELINE_CONFIG.rawColors.gapInactive,
opacity: isClearing ? 0 : 1,
}}
transition={{
width: {
duration: 0, // No delay for width animation
},
backgroundColor: TIMELINE_CONFIG.animation.activeToInactive,
opacity: {
duration: 0.3,
ease: "easeInOut",
},
}}
/>
{/* Duration label for gap segments */}
{segmentDuration !== null && segmentWidth > 50 && (
<div
className="absolute pointer-events-none"
style={{
left: `${segmentStartX - 16 + segmentWidth / 2}px`,
top: `${TIMELINE_CONFIG.height / 2}px`,
transform: "translate(-50%, -50%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<motion.div
className="bg-neutral-900/90 px-2 py-0.5 rounded text-xs font-mono text-neutral-300 border border-neutral-700 whitespace-nowrap"
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: isClearing ? 0 : 1,
scale: 1,
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
>
{formatTime(segmentDuration)}
</motion.div>
</div>
)}
</React.Fragment>
)
}
})}
{/* Cursor - vertical line that changes color when stopped */}
<motion.div
className="absolute"
style={{
left: `${currentX - scrollOffset - 16}px`,
width: `${TIMELINE_CONFIG.cursorWidth}px`,
height: `${TIMELINE_CONFIG.height}px`,
top: "0",
}}
animate={{
left: `${currentX - scrollOffset - 16}px`,
backgroundColor: isActive
? TIMELINE_CONFIG.rawColors.cursorActive
: TIMELINE_CONFIG.rawColors.cursorInactive,
opacity: isClearing ? 0 : 1,
}}
transition={{
left: {
duration: TIMELINE_CONFIG.animation.cursorDuration,
ease: "linear",
},
backgroundColor: {
duration: 0.3,
ease: "easeInOut",
},
opacity: {
duration: 0.8,
ease: "easeInOut",
},
}}
/>
</div>
</div>
</div>
)
}
================================================
FILE: src/components/Timer.tsx
================================================
import { useEffect, useRef, useState } from "react"
import type { VisualEffect } from "@/VisualEffect"
function useTimer(task: VisualEffect<unknown, unknown>) {
const [elapsedTime, setElapsedTime] = useState(0)
const intervalRef = useRef<number | null>(null)
useEffect(() => {
if (task.showTimer && task.state.type === "running" && task.startTime) {
const start = task.startTime
const updateTimer = () => {
const now = Date.now()
setElapsedTime(now - (start ?? now))
}
updateTimer()
intervalRef.current = window.setInterval(updateTimer, 10)
} else if (task.showTimer && task.endTime && task.startTime) {
setElapsedTime(task.endTime - task.startTime)
} else if (task.state.type === "idle") {
setElapsedTime(0)
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
}, [task.showTimer, task.state.type, task.startTime, task.endTime])
return elapsedTime
}
export function Timer({ effect: task }: { effect: VisualEffect<unknown, unknown> }) {
const elapsedTime = useTimer(task)
const shouldShowTimer =
task.showTimer &&
(task.state.type === "running" ||
task.state.type === "completed" ||
task.state.type === "failed" ||
task.state.type === "interrupted" ||
task.state.type === "death")
const formatTime = (ms: number) => {
if (ms < 1000) {
return `${ms}ms`
} else {
return `${(ms / 1000).toFixed(1)}s`
}
}
if (shouldShowTimer) {
return <span className="font-mono">{formatTime(elapsedTime)}</span>
}
return <span>{task.name}</span>
}
================================================
FILE: src/components/display/EffectExample.tsx
================================================
"use client"
import { ArrowRightIcon } from "@phosphor-icons/react"
import { motion } from "motion/react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import type { VisualEffect } from "../../VisualEffect"
import type { VisualRef } from "../../VisualRef"
import type { VisualScope } from "../../VisualScope"
import { CodeBlock } from "../CodeBlock"
import { EffectNode } from "../effect"
import { FloatingHighlight } from "../feedback"
import { HeaderView } from "../HeaderView"
import { ScopeStack } from "../scope/ScopeStack"
import { RefDisplay, ScheduleTimeline } from "./"
export interface EffectHighlight {
text: string
}
export interface EffectExampleProps<A, E> {
name: string
variant?: string
description: React.ReactNode
code: string
effects: Array<VisualEffect<unknown, unknown>>
resultEffect?: VisualEffect<A, E>
effectHighlightMap: Record<string, EffectHighlight | undefined>
index?: number
showScheduleTimeline?: boolean
isDarkMode?: boolean
configurationPanel?: React.ReactNode
refs?: Array<VisualRef<unknown>>
scope?: VisualScope
exampleId: string
}
// Default empty array to avoid recreating on every render
const EMPTY_REFS_ARRAY: Array<VisualRef<unknown>> = []
function EffectExampleComponent<A, E>({
code,
configurationPanel,
description,
exampleId,
isDarkMode = false,
name,
refs = EMPTY_REFS_ARRAY,
resultEffect,
scope,
showScheduleTimeline,
effectHighlightMap,
effects,
variant,
}: EffectExampleProps<A, E>) {
const [hoveredEffect, setHoveredEffect] = useState<string | null>(null)
const [delayedHoveredEffect, setDelayedHoveredEffect] = useState<string | null>(null)
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const codeContainerRef = useRef<HTMLDivElement>(null)
// Memoize hover handlers to prevent re-creation on every render
const handleMouseEnter = useCallback((effectName: string) => {
setHoveredEffect(effectName)
}, [])
const handleMouseLeave = useCallback(() => {
setHoveredEffect(null)
}, [])
// Handle hover with delay
useEffect(() => {
// Clear any existing timeout
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current)
hoverTimeoutRef.current = null
}
if (hoveredEffect) {
// Immediately show highlight when hovering
setDelayedHoveredEffect(hoveredEffect)
} else {
// Delay hiding the highlight
hoverTimeoutRef.current = setTimeout(() => {
setDelayedHoveredEffect(null)
}, 500) // 500ms delay before hiding
}
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current)
}
}
}, [hoveredEffect])
// Determine if this is a single effect example
const isSingleEffect = !resultEffect || (effects.length === 1 && effects[0] === resultEffect)
const headerEffect = resultEffect || effects[0]
if (!headerEffect) {
throw new Error("EffectExample requires at least one effect")
}
// Shared UI values
const borderColorValue = isDarkMode ? "rgba(127, 29, 29, 0.5)" : "rgba(64, 64, 64, 0.5)"
const backgroundGradient = isDarkMode
? "linear-gradient(to bottom right, black, rgba(127, 29, 29, 0.2))"
: "linear-gradient(to bottom right, rgba(23, 23, 23, 0.8), rgba(23, 23, 23, 0.4))"
const headerBackground = isDarkMode ? "rgba(0, 0, 0, 0.5)" : "rgba(38, 38, 38, 0.5)"
const standardTransition = { duration: 0.2, ease: "easeInOut" as const }
const highlightTarget = delayedHoveredEffect
? effectHighlightMap[delayedHoveredEffect] || null
: null
// If the result node should display elapsed ms from a timer-enabled effect, prefer its own timer,
// otherwise fall back to the first input effect that has a timer.
const labelEffectForResult: VisualEffect<unknown, unknown> | undefined = resultEffect?.showTimer
? (resultEffect as unknown as VisualEffect<unknown, unknown>)
: effects.find(e => e.showTimer)
return (
<motion.div
className={`w-full flex flex-col border rounded-2xl shadow-2xl relative `}
initial={{
boxShadow: isDarkMode ? `0 0 40px rgba(220, 38, 38, 0.3)` : `0 0 0 0 rgba(59, 130, 250, 0)`,
borderColor: borderColorValue,
background: backgroundGradient,
}}
animate={{
boxShadow: isDarkMode ? `0 0 40px rgba(220, 38, 38, 0.3)` : `0 0 0 0 rgba(59, 130, 250, 0)`,
borderColor: borderColorValue,
background: backgroundGradient,
}}
transition={{
borderColor: standardTransition,
background: standardTransition,
}}
>
{/* Header with interactive controls */}
<motion.div
className={`p-4 border-b rounded-t-2xl`}
initial={{
borderColor: borderColorValue,
backgroundColor: headerBackground,
}}
animate={{
borderColor: borderColorValue,
backgroundColor: headerBackground,
}}
transition={standardTransition}
>
<HeaderView
effect={headerEffect}
name={name}
{...(variant && { variant })}
description={description}
refs={refs}
exampleId={exampleId}
/>
</motion.div>
{/* Configuration Panel */}
{configurationPanel && (
<motion.div
initial={{
borderColor: borderColorValue,
}}
animate={{
borderColor: borderColorValue,
}}
transition={standardTransition}
className="border-b"
>
{configurationPanel}
</motion.div>
)}
{/* Refs display */}
{refs.length > 0 && (
<motion.div
className="p-4 border-b"
initial={{
borderColor: borderColorValue,
}}
animate={{
borderColor: borderColorValue,
}}
transition={standardTransition}
>
<div className="flex flex-wrap gap-3">
{refs.map(ref => (
<RefDisplay key={ref.name} visualRef={ref} />
))}
</div>
</motion.div>
)}
{/* Main visualization */}
<motion.div
className={`px-4 py-5 border-b`}
initial={{
borderColor: borderColorValue,
}}
animate={{
borderColor: borderColorValue,
}}
transition={standardTransition}
>
{isSingleEffect ? (
// Single effect - just show the effect
<div className="flex justify-start">
<div
onMouseEnter={() => handleMouseEnter(headerEffect.name)}
onMouseLeave={handleMouseLeave}
>
<EffectNode effect={headerEffect} />
</div>
</div>
) : (
// Multiple effects with arrow and result
<div className="flex flex-row items-center justify-start gap-6">
{/* Input effects - wrap on mobile */}
<div className="flex flex-wrap justify-center gap-6">
{effects.map(effect => (
<div
key={effect.name}
onMouseEnter={() => handleMouseEnter(effect.name)}
onMouseLeave={handleMouseLeave}
>
<EffectNode effect={effect} />
</div>
))}
</div>
{/* Arrow - rotate on mobile */}
<div className="text-neutral-500 rotate-0 flex items-center relative top-[-13px]">
<ArrowRightIcon size={24} weight="fill" />
</div>
{/* Result */}
{resultEffect && (
<div
onMouseEnter={() => handleMouseEnter(resultEffect.name)}
onMouseLeave={handleMouseLeave}
>
<EffectNode
effect={resultEffect}
{...(labelEffectForResult && { labelEffect: labelEffectForResult })}
/>
</div>
)}
</div>
)}
</motion.div>
{/* Schedule timeline (if provided) */}
{showScheduleTimeline && effects[0] && resultEffect && (
<motion.div
initial={{
borderColor: borderColorValue,
}}
animate={{
borderColor: borderColorValue,
}}
transition={standardTransition}
className="border-b"
>
<ScheduleTimeline baseEffect={effects[0]} repeatEffect={resultEffect} />
</motion.div>
)}
{/* Scope visualization (if provided) */}
{scope && (
<motion.div
initial={{
borderColor: borderColorValue,
}}
animate={{
borderColor: borderColorValue,
}}
transition={standardTransition}
className="border-b"
>
<ScopeStack scope={scope} />
</motion.div>
)}
{/* Code block */}
<div
className="relative p-4 text-base"
ref={codeContainerRef}
style={{ position: "relative" }}
>
<CodeBlock code={code} activeLines={[]} />
<FloatingHighlight
containerRef={codeContainerRef as React.RefObject<HTMLDivElement>}
target={highlightTarget}
/>
</div>
</motion.div>
)
}
// Helper function to compare arrays by reference and length
function areArraysEqual<T>(a: T[] | undefined, b: T[] | undefined): boolean {
if (a === b) return true
if (!a || !b) return a === b
if (a.length !== b.length) return false
return a.every((item, index) => item === b[index])
}
// Memoized component with custom comparison function
export const EffectExample = memo(EffectExampleComponent, (prevProps, nextProps) => {
// Compare all props except functions and objects that might have new references
return (
prevProps.name === nextProps.name &&
prevProps.variant === nextProps.variant &&
prevProps.code === nextProps.code &&
prevProps.index === nextProps.index &&
prevProps.showScheduleTimeline === nextProps.showScheduleTimeline &&
prevProps.isDarkMode === nextProps.isDarkMode &&
areArraysEqual(prevProps.effects, nextProps.effects) &&
prevProps.resultEffect === nextProps.resultEffect &&
areArraysEqual(prevProps.refs, nextProps.refs) &&
prevProps.scope === nextProps.scope &&
prevProps.exampleId === nextProps.exampleId &&
prevProps.effectHighlightMap === nextProps.effectHighlightMap
)
}) as typeof EffectExampleComponent
================================================
FILE: src/components/display/RefDisplay.tsx
================================================
import { AnimatePresence, motion, type Transition } from "motion/react"
import { useVisualRef, type VisualRef } from "@/VisualRef"
interface RefDisplayProps<A> {
visualRef: VisualRef<A>
style?: React.CSSProperties
}
export function RefDisplay<A>({ style = {}, visualRef }: RefDisplayProps<A>) {
const { justChanged, value } = useVisualRef(visualRef)
// Dynamic transition: quick flash in (50ms) then slow fade out (600ms)
const transition: Transition = {
visualDuration: justChanged ? 0.1 : 0.3,
bounce: 0,
type: "spring",
}
return (
<div className="flex" style={{ ...style, position: "relative", flex: "1 1 auto" }}>
<motion.div
className="flex items-center rounded-lg border"
initial={{
backgroundColor: "rgba(38, 38, 38, 0.8)",
borderColor: "rgba(64, 64, 64, 0.5)",
}}
animate={{
backgroundColor: justChanged
? "rgba(59, 130, 246, 0.3)" // brighter on flash
: "rgba(38, 38, 38, 0.8)",
borderColor: justChanged ? "rgba(59, 130, 246, 1)" : "rgba(64, 64, 64, 0.5)",
}}
transition={transition}
>
{/* Ref name */}
<span className="text-md font-medium whitespace-nowrap text-neutral-400 p-2 px-4">
{visualRef.name}
</span>
{/* Vertical separator */}
<motion.span
className="w-px h-full "
initial={{ backgroundColor: "rgba(64, 64, 64, 0.5)" }}
animate={{
backgroundColor: justChanged ? "rgba(59, 130, 246, 0.3)" : "rgba(64, 64, 64, 0.5)",
}}
exit={{
backgroundColor: "rgba(64, 64, 64, 0.5)",
transition: { duration: 0.6, ease: "easeInOut" },
}}
transition={transition}
/>
{/* Ref value with odometer-style transition */}
<div className="text-md font-mono font-semibold text-neutral-100 min-w-0 overflow-hidden p-2 px-4">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={String(value)}
initial={{ y: -8, opacity: 0, filter: "blur(4px)" }}
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
exit={{ y: 8, opacity: 0, filter: "blur(4px)" }}
transition={{ duration: 0.35, ease: "easeOut" }}
className="inline-block"
>
{String(value)}
</motion.span>
</AnimatePresence>
</div>
</motion.div>
</div>
)
}
================================================
FILE: src/components/display/index.ts
================================================
export { ScheduleTimeline } from "../ScheduleTimeline"
export { EffectExample } from "./EffectExample"
export { RefDisplay } from "./RefDisplay"
================================================
FILE: src/components/effect/EffectContainer.tsx
================================================
import { motion, useTransform } from "motion/react"
import { colors, effects } from "@/animations"
import type { VisualEffect } from "../../VisualEffect"
import { nodeVariants } from "./nodeVariants"
import { getTaskShadow } from "./taskUtils"
import type { EffectMotionValues } from "./useEffectMotion"
interface EffectContainerProps {
state: VisualEffect<unknown, unknown>["state"]
motionValues: Pick<
EffectMotionValues,
| "nodeWidth"
| "nodeHeight"
| "borderRadius"
| "rotation"
| "shakeX"
| "shakeY"
| "blurAmount"
| "glowIntensity"
>
onMouseEnter?: () => void
onMouseLeave?: () => void
children: React.ReactNode
}
export function EffectContainer({
motionValues,
children,
onMouseEnter,
onMouseLeave,
state,
}: EffectContainerProps) {
const isDeath = state.type === "death"
// Use variants for static state-based properties
const current = state.type as keyof typeof nodeVariants
return (
<motion.div
// Hybrid approach: variants handle static properties
variants={nodeVariants}
animate={current}
initial={false}
style={{
// Imperative motion values drive dynamic sizing
width: motionValues.nodeWidth,
height: motionValues.nodeHeight,
borderRadius: motionValues.borderRadius,
position: "absolute",
overflow: "hidden",
// Variants still own scale, opacity, background color
rotate: motionValues.rotation,
x: motionValues.shakeX,
y: motionValues.shakeY,
cursor: "auto",
border: isDeath ? `2px solid ${colors.border.death}` : `1px solid ${colors.border.default}`,
// Promote to its own GPU layer and limit reflows/paints
contain: "layout style paint", // restrict the scope of layout and paint work
willChange: "transform, filter",
transform: "translateZ(0)", // ensure GPU compositing
filter: useTransform([motionValues.blurAmount], ([blur = 0]: Array<number>) => {
// Cap blur radius to 2px max for better performance
const cappedBlur = Math.min(blur, 2)
return isDeath
? `blur(${cappedBlur}px) contrast(${effects.death.contrast}) brightness(${effects.death.brightness})`
: `blur(${cappedBlur}px)`
}),
// Use box-shadow for glow instead of expensive drop-shadow
boxShadow: useTransform([motionValues.glowIntensity], ([glow = 0]: Array<number>) => {
const cappedGlow = Math.min(glow, 8)
const baseGlow = getTaskShadow(state)
if (isDeath) {
return cappedGlow > 0
? `${baseGlow}, 0 0 ${cappedGlow * 2}px ${colors.glow.death}`
: baseGlow
}
return cappedGlow > 0
? `${baseGlow}, 0 0 ${cappedGlow}px ${colors.glow.running}`
: baseGlow
}),
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</motion.div>
)
}
================================================
FILE: src/components/effect/EffectContent.tsx
================================================
import { SkullIcon, StarFourIcon, WarningOctagonIcon } from "@phosphor-icons/react"
import { AnimatePresence, motion } from "motion/react"
import { useLayoutEffect, useRef } from "react"
import { springs } from "@/animations"
import { theme } from "../../theme"
import type { VisualEffect } from "../../VisualEffect"
import { isRenderableResult, renderResult } from "../renderers"
import type { EffectMotionValues } from "./useEffectMotion"
type EffectState = VisualEffect<unknown, unknown>["state"]
interface EffectContentProps {
state: EffectState
motionValues: Pick<EffectMotionValues, "contentOpacity" | "contentScale" | "nodeWidth">
}
function TaskIcon({ size, type }: { type: string; size: number }) {
const iconSize = size * 0.5
const iconProps = {
size: iconSize,
color: "rgba(255, 255, 255, 0.9)",
}
switch (type) {
case "failed":
return <SkullIcon {...iconProps} weight="fill" />
case "death":
return <SkullIcon {...iconProps} weight="fill" color="#dc2626" />
case "interrupted":
return <WarningOctagonIcon {...iconProps} weight="fill" />
default:
return <StarFourIcon {...iconProps} weight="fill" />
}
}
function TaskContentInner({ state }: { state: EffectState }) {
switch (state.type) {
case "failed":
case "interrupted":
case "death":
return (
<motion.div
key={state.type}
initial={{ scale: 0, filter: "blur(10px)" }}
animate={{ scale: 1, filter: "blur(0px)" }}
exit={{ scale: 0, filter: "blur(10px)" }}
transition={{
type: "spring",
bounce: state.type === "interrupted" ? 0.5 : 0.3,
visualDuration: 0.3,
}}
>
<TaskIcon type={state.type} size={64} />
</motion.div>
)
case "completed": {
const { result } = state
const content = isRenderableResult(result) ? renderResult(result) : String(result)
return (
<motion.div
key="result"
initial={{ opacity: 0, scale: 0.5, filter: "blur(10px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.5, filter: "blur(10px)" }}
transition={{
...springs.bouncy,
stiffness: 260,
damping: 18,
}}
>
{content}
</motion.div>
)
}
case "running":
return null
default:
return (
<motion.div
key="star"
initial={{ scale: 0, filter: "blur(10px)" }}
animate={{ scale: 1, filter: "blur(0px)" }}
exit={{ scale: 0, filter: "blur(10px)" }}
transition={{ type: "spring", bounce: 0.3, visualDuration: 0.3 }}
>
<TaskIcon type="default" size={64} />
</motion.div>
)
}
}
export function EffectContent({ motionValues, state }: EffectContentProps) {
const contentRef = useRef<HTMLDivElement>(null)
// Auto-resize width based on content (synchronous, before paint)
useLayoutEffect(() => {
if (state.type === "completed" && contentRef.current) {
const actualWidth = contentRef.current.scrollWidth
if (actualWidth > 64 - 16) {
motionValues.nodeWidth.set(actualWidth + 24)
}
}
}, [state, motionValues.nodeWidth])
return (
<motion.div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 600,
color: theme.colors.textPrimary,
opacity: motionValues.contentOpacity,
scale: motionValues.contentScale,
padding: "0 8px",
}}
>
<div ref={contentRef} style={{ whiteSpace: "nowrap" }}>
<AnimatePresence mode="popLayout">
<TaskContentInner state={state} />
</AnimatePresence>
</div>
</motion.div>
)
}
================================================
FILE: src/components/effect/EffectLabel.tsx
================================================
import { motion } from "motion/react"
import { Timer } from "@/components/Timer"
import { theme } from "@/theme"
import type { VisualEffect } from "@/VisualEffect"
import { useVisualEffectState } from "@/VisualEffect"
interface EffectLabelProps {
effect: VisualEffect<unknown, unknown>
}
export function EffectLabel({ effect }: EffectLabelProps) {
const state = useVisualEffectState(effect)
return (
<motion.div
style={{
marginTop: theme.spacing.sm,
fontSize: "0.75rem",
textAlign: "center",
fontWeight: 500,
color: theme.colors.textMuted,
}}
animate={{
color: state.type === "idle" ? theme.colors.textMuted : theme.colors.textSecondary,
}}
transition={{ duration: 0.3 }}
>
<Timer effect={effect} />
</motion.div>
)
}
================================================
FILE: src/components/effect/EffectNode.tsx
================================================
import { AnimatePresence, motion } from "motion/react"
import { memo, useCallback, useState } from "react"
import {
useVisualEffectNotification,
useVisualEffectState,
type VisualEffect,
} from "@/VisualEffect"
import { DeathBubble } from "../feedback/DeathBubble"
import { FailureBubble } from "../feedback/FailureBubble"
import { NotificationBubble } from "../feedback/NotificationBubble"
import { EffectContainer } from "./EffectContainer"
import { EffectContent } from "./EffectContent"
import { EffectLabel } from "./EffectLabel"
import { EffectOverlay } from "./EffectOverlay"
import {
useEffectAnimations,
useEffectMotion,
useRunningAnimation,
useStateAnimations,
} from "./useEffectMotion"
function EffectNodeComponent<A, E>({
style = {},
effect,
labelEffect,
}: {
effect: VisualEffect<A, E>
style?: React.CSSProperties
labelEffect?: VisualEffect<unknown, unknown>
}) {
const notification = useVisualEffectNotification(effect)
const state = useVisualEffectState(effect)
const effectMotion = useEffectMotion()
const isRunning = state.type === "running"
const isFailedOrDeath = state.type === "failed" || state.type === "death"
// State for error bubble visibility
const [showErrorBubble, setShowErrorBubble] = useState(false)
const [isHovering, setIsHovering] = useState(false)
// Apply all animations
useRunningAnimation(isRunning, effectMotion)
useStateAnimations(state, effectMotion)
useEffectAnimations(state, effectMotion, isHovering, setShowErrorBubble)
// Stable mouse handlers to avoid creating new functions on every render
const handleMouseEnter = useCallback(() => {
setIsHovering(true)
if (isFailedOrDeath) setShowErrorBubble(true)
}, [isFailedOrDeath])
const handleMouseLeave = useCallback(() => {
setIsHovering(false)
}, [])
return (
<div style={{ ...style, position: "relative" }}>
{/* Error bubble positioned outside container */}
<AnimatePresence>
{isFailedOrDeath &&
showErrorBubble &&
(state.type === "failed" ? (
<FailureBubble error={state.error} />
) : (
<DeathBubble error={state.error} />
))}
</AnimatePresence>
{/* Notification bubbles - hidden when error bubbles are shown */}
<AnimatePresence>
{!isFailedOrDeath && notification && (
<NotificationBubble key={notification.id} notification={notification} />
)}
</AnimatePresence>
<motion.div
style={{
width: effectMotion.nodeWidth,
height: 64,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}
>
<EffectContainer
state={state}
motionValues={effectMotion}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<EffectOverlay isRunning={isRunning} motionValues={effectMotion} />
<EffectContent state={state} motionValues={effectMotion} />
</EffectContainer>
</motion.div>
<EffectLabel effect={labelEffect ?? effect} />
</div>
)
}
export const EffectNode = memo(EffectNodeComponent) as typeof EffectNodeComponent
================================================
FILE: src/components/effect/EffectOverlay.tsx
================================================
import { motion } from "motion/react"
import { theme } from "../../theme"
import type { EffectMotionValues } from "./useEffectMotion"
interface EffectOverlayProps {
isRunning: boolean
motionValues: Pick<
EffectMotionValues,
"borderRadius" | "borderOpacity" | "flashOpacity" | "flashColor"
>
}
function RunningOverlay() {
return (
<>
{[0, 0.2, 0.4, 0.6, 0.8, 1].map((delay, i) => (
<motion.div
key={i}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: "200%",
background:
"linear-gradient(90deg, transparent 0%, transparent 40%, rgba(255,255,255,0.1) 45%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0.1) 55%, transparent 60%, transparent 100%)",
filter: "blur(4px)",
mixBlendMode: "lighten",
}}
animate={{
x: ["-66.0%", "50%"],
}}
transition={{
duration: 0.8,
delay,
repeat: Infinity,
ease: [0.5, 0, 0.1, 1],
}}
/>
))}
</>
)
}
export function EffectOverlay({ motionValues, isRunning }: EffectOverlayProps) {
return (
<>
{/* Animated border overlay for running state */}
{isRunning && (
<motion.div
style={{
position: "absolute",
inset: 0,
borderRadius: motionValues.borderRadius,
boxShadow: "inset 0 0 0 1px rgba(100, 200, 255, 0.8)",
opacity: motionValues.borderOpacity,
pointerEvents: "none",
}}
/>
)}
{/* Running animation overlay */}
{isRunning && <RunningOverlay />}
{/* Flash effect overlay */}
<motion.div
style={{
position: "absolute",
inset: 0,
borderRadius: theme.radius.md,
background: motionValues.flashColor,
mixBlendMode: "overlay",
opacity: motionValues.flashOpacity,
pointerEvents: "none",
}}
/>
</>
)
}
================================================
FILE: src/components/effect/index.ts
================================================
export { EffectContainer } from "./EffectContainer"
export { EffectContent } from "./EffectContent"
export { EffectLabel } from "./EffectLabel"
export { EffectNode } from "./EffectNode"
export { EffectOverlay } from "./EffectOverlay"
export { nodeVariants } from "./nodeVariants"
export * from "./taskUtils"
export type { EffectMotionValues } from "./useEffectMotion"
export {
useEffectAnimations,
useEffectMotion,
useRunningAnimation,
useStateAnimations,
} from "./useEffectMotion"
================================================
FILE: src/components/effect/nodeVariants.ts
================================================
import { springs } from "@/animations"
import { TASK_COLORS } from "../../constants/colors"
// Hybrid approach: Only handle static state-based properties in variants
// Keep complex animations (jitter, pulses, flashes, dynamic sizing) imperative
export const nodeVariants = {
idle: {
scale: 1,
opacity: 0.6,
backgroundColor: TASK_COLORS.idle,
transition: {
// Fast color change to match original
backgroundColor: { duration: 0.1, ease: "easeInOut" },
// Keep spring for scale/opacity
scale: springs.default,
opacity: springs.default,
},
},
running: {
scale: 0.95,
opacity: 1,
backgroundColor: TASK_COLORS.running,
transition: {
backgroundColor: { duration: 0.1, ease: "easeInOut" },
scale: springs.default,
opacity: springs.default,
},
},
completed: {
scale: 1,
opacity: 1,
backgroundColor: TASK_COLORS.success,
transition: {
backgroundColor: { duration: 0.1, ease: "easeInOut" },
scale: springs.contentScale,
opacity: springs.contentScale,
},
},
failed: {
backgroundColor: TASK_COLORS.error,
scale: 1,
opacity: 1,
transition: {
backgroundColor: { duration: 0.1, ease: "easeInOut" },
scale: springs.contentScale,
opacity: springs.contentScale,
},
},
death: {
backgroundColor: TASK_COLORS.death,
scale: 1,
opacity: 1,
transition: {
backgroundColor: { duration: 0.1, ease: "easeInOut" },
scale: springs.contentScale,
opacity: springs.contentScale,
},
},
interrupted: {
backgroundColor: TASK_COLORS.interrupted,
opacity: 1,
scale: 1,
transition: {
backgroundColor: { duration: 0.1, ease: "easeInOut" },
scale: springs.default,
opacity: springs.default,
},
},
} as const
// Note: Width, height, and complex animations remain imperative
// This preserves dynamic width expansion and complex timing sequences
================================================
FILE: src/components/effect/taskUtils.ts
================================================
import { SHADOW_COLORS } from "../../constants/colors"
import { theme } from "../../theme"
import type { VisualEffect } from "../../VisualEffect"
type TaskState = VisualEffect<unknown, unknown>["state"]
export function getTaskShadow(state: TaskState): string {
switch (state.type) {
case "running":
return SHADOW_COLORS.running
default:
return theme.shadow.sm
}
}
================================================
FILE: src/components/effect/useEffectMotion.ts
================================================
import {
type AnimationPlaybackControls,
animate,
type MotionValue,
useMotionValue,
useSpring,
useTransform,
useVelocity,
} from "motion/react"
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { colors, effects, shake, springs, timing } from "@/animations"
import { dimensions } from "@/constants/dimensions"
import { useStateTransition } from "../../hooks/useStateTransition"
import { theme } from "../../theme"
import type { VisualEffect } from "../../VisualEffect"
type EffectState = VisualEffect<unknown, unknown>["state"]
export interface EffectMotionValues {
nodeWidth: MotionValue<number>
nodeHeight: MotionValue<number>
contentOpacity: MotionValue<number>
flashOpacity: MotionValue<number>
flashColor: MotionValue<string>
borderRadius: MotionValue<number>
rotation: MotionValue<number>
shakeX: MotionValue<number>
shakeY: MotionValue<number>
contentScale: MotionValue<number>
blurAmount: MotionValue<number>
borderColor: MotionValue<string>
borderOpacity: MotionValue<number>
glowIntensity: MotionValue<number>
}
/** tiny util: stable init once without eslint disables */
function useConst<T>(init: () => T): T {
const ref = useRef<T | null>(null)
if (ref.current === null) ref.current = init()
return ref.current
}
/** minimal, SSR-safe reduced-motion hook */
function usePrefersReducedMotion() {
const [prefers, setPrefers] = useState(false)
useEffect(() => {
if (typeof window === "undefined" || !("matchMedia" in window)) return
const mql = window.matchMedia("(prefers-reduced-motion: reduce)")
const set = () => setPrefers(mql.matches)
set()
// legacy Safari support
const onChange = (e: MediaQueryListEvent) => setPrefers(e.matches)
mql.addEventListener ? mql.addEventListener("change", onChange) : mql.addListener(onChange)
return () => {
mql.removeEventListener
? mql.removeEventListener("change", onChange)
: mql.removeListener(onChange)
}
}, [])
return prefers
}
// ------------------------
// useEffectMotion
// ------------------------
export function useEffectMotion(): EffectMotionValues {
const nodeWidth = useSpring(dimensions.node.width, springs.nodeWidth)
const contentOpacity = useSpring(1, springs.default)
const flashOpacity = useMotionValue(0)
const flashColor = useMotionValue<string>(colors.flash)
const borderRadius = useSpring(theme.radius.md, springs.default)
const nodeHeight = useMotionValue(64)
const rotation = useMotionValue(0)
const shakeX = useMotionValue(0)
const shakeY = useMotionValue(0)
const contentScale = useSpring(1, springs.default)
const borderColor = useMotionValue<string>(colors.border.default)
const borderOpacity = useSpring(1, springs.default)
const glowIntensity = useSpring(0, springs.default)
const rotationVelocity = useVelocity(rotation)
// Optional smoothing of velocity -> blur to avoid flicker
// const smoothedVel = useSpring(rotationVelocity, { stiffness: 200, damping: 40 })
const blurAmount = useTransform(rotationVelocity, [-100, 0, 100], [1, 0, 1], { clamp: true })
// Stable container without eslint disables
const motionValues = useConst<EffectMotionValues>(() => ({
nodeWidth,
nodeHeight,
contentOpacity,
flashOpacity,
flashColor,
borderRadius,
rotation,
shakeX,
shakeY,
contentScale,
blurAmount,
borderColor,
borderOpacity,
glowIntensity,
}))
return motionValues
}
// ------------------------
// useRunningAnimation
// ------------------------
export function useRunningAnimation(
isRunning: boolean,
motionValues: Pick<
EffectMotionValues,
"rotation" | "shakeX" | "shakeY" | "borderOpacity" | "glowIntensity"
>,
) {
const prefersReducedMotion = usePrefersReducedMotion()
useEffect(() => {
let cancelled = false
let rafId: number | null = null // single RAF id (fix leak)
const animControls: {
border: AnimationPlaybackControls | undefined
glow: AnimationPlaybackControls | undefined
} = { border: undefined, glow: undefined }
const stopAll = () => {
if (animControls.border) animControls.border.stop()
if (animControls.glow) animControls.glow.stop()
if (rafId !== null) cancelAnimationFrame(rafId)
// reset quickly
animate(motionValues.rotation, 0, {
duration: timing.exit.duration,
ease: timing.exit.ease,
})
animate(motionValues.shakeX, 0, {
duration: timing.exit.duration,
ease: timing.exit.ease,
})
animate(motionValues.shakeY, 0, {
duration: timing.exit.duration,
ease: timing.exit.ease,
})
motionValues.borderOpacity.set(1)
motionValues.glowIntensity.set(0)
}
if (!isRunning || prefersReducedMotion) {
stopAll()
return
}
// border pulse
animControls.border = animate(motionValues.borderOpacity, [...timing.borderPulse.values], {
duration: timing.borderPulse.duration,
ease: "easeInOut",
repeat: Infinity,
})
// glow pulse
animControls.glow = animate(motionValues.glowIntensity, [...timing.glowPulse.values], {
duration: timing.glowPulse.duration,
ease: "easeInOut",
repeat: Infinity,
})
const jitter = () => {
if (cancelled) return
const angle =
(Math.random() * shake.running.angleRange + shake.running.angleBase) *
(Math.random() < 0.5 ? 1 : -1)
const offset =
(Math.random() * shake.running.offsetRange + shake.running.offsetBase) *
(Math.random() < 0.5 ? -1 : 1)
const offsetY =
(Math.random() * shake.running.offsetYRange + shake.running.offsetYBase) *
(Math.random() < 0.5 ? -1 : 1)
// FIX: proper [min,max] duration
const min = shake.running.durationMin
const max = shake.running.durationMax ?? min * 2
const duration = min + Math.random() * Math.max(0.001, max - min)
const rot = animate(motionValues.rotation, angle, { duration, ease: "circInOut" })
const x = animate(motionValues.shakeX, offset, { duration, ease: "easeInOut" })
const y = animate(motionValues.shakeY, offsetY, { duration, ease: "easeInOut" })
// When this triple finishes, schedule next cycle on next frame
Promise.all([rot.finished, x.finished, y.finished]).then(() => {
if (cancelled) return
rafId = requestAnimationFrame(jitter)
})
}
rafId = requestAnimationFrame(jitter)
return () => {
cancelled = true
stopAll()
}
}, [
isRunning,
motionValues.rotation,
motionValues.shakeX,
motionValues.shakeY,
motionValues.borderOpacity,
motionValues.glowIntensity,
])
}
// ------------------------
// useStateAnimations
// ------------------------
export function useStateAnimations(state: EffectState, motionValues: EffectMotionValues) {
const isRunning = state.type === "running"
// Radius
useEffect(() => {
motionValues.borderRadius.set(isRunning ? 15 : theme.radius.md)
}, [isRunning, motionValues.borderRadius])
// Height
useEffect(() => {
animate(motionValues.nodeHeight, isRunning ? 64 * 0.4 : 64, {
duration: 0.4,
bounce: isRunning ? 0.3 : 0.5,
type: "spring",
})
}, [isRunning, motionValues.nodeHeight])
// Width & content opacity
useEffect(() => {
const hasResult = state.type === "completed"
if (!hasResult) {
motionValues.nodeWidth.set(64) // if you want this animated, swap to animate(...)
}
motionValues.contentOpacity.set(hasResult ? 1 : state.type === "running" ? 0 : 1)
}, [state, motionValues, isRunning])
}
// ------------------------
// useEffectAnimations
// ------------------------
export function useEffectAnimations(
state: EffectState,
motionValues: EffectMotionValues,
isHovering: boolean,
setShowErrorBubble: (show: boolean) => void,
) {
const prefersReducedMotion = usePrefersReducedMotion()
const isFailish = state.type === "failed" || state.type === "death"
// Error bubble visibility (comment aligned with code: 1.5s)
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | undefined
if (isFailish) {
setShowErrorBubble(true)
timer = setTimeout(() => {
if (!isHovering) setShowErrorBubble(false)
}, 1500)
} else {
setShowErrorBubble(false)
}
return () => {
if (timer) clearTimeout(timer)
}
}, [isFailish, isHovering, setShowErrorBubble])
// Flash on start/complete
const transition = useStateTransition(state)
useEffect(() => {
if (transition.justCompleted || transition.justStarted) {
const up = animate(motionValues.flashOpacity, 0.6, {
duration: 0.02,
ease: "circOut",
})
up.finished.then(() => {
if (prefersReducedMotion) {
motionValues.flashOpacity.set(0)
} else {
animate(motionValues.flashOpacity, 0, {
duration: timing.flash.duration,
ease: timing.flash.ease,
})
}
})
}
}, [
transition.justCompleted,
transition.justStarted,
motionValues.flashOpacity,
prefersReducedMotion,
])
// Failure/Death shake
useEffect(() => {
if (!(state.type === "failed" || state.type === "death") || prefersReducedMotion) return
let cancelled = false
const shakeSequence = async () => {
const { intensity, duration, count, rotationRange, returnDuration } = shake.failure
for (let i = 0; i < count && !cancelled; i++) {
const xOffset = (Math.random() - 0.5) * intensity
const yOffset = (Math.random() - 0.5) * intensity
const rotOffset = (Math.random() - 0.5) * rotationRange
const anims = [
animate(motionValues.shakeX, xOffset, { duration, ease: "easeInOut" }),
animate(motionValues.shakeY, yOffset, { duration, ease: "easeInOut" }),
animate(motionValues.rotation, rotOffset, { duration, ease: "easeInOut" }),
]
await Promise.all(anims.map(a => a.finished))
}
if (!cancelled) {
await Promise.all([
animate(motionValues.shakeX, 0, { duration: returnDuration, ease: "easeOut" }).finished,
animate(motionValues.shakeY, 0, { duration: returnDuration, ease: "easeOut" }).finished,
animate(motionValues.rotation, 0, { duration: returnDuration, ease: "easeOut" }).finished,
])
}
}
shakeSequence()
return () => {
cancelled = true
}
}, [state, prefersReducedMotion, motionValues.shakeX, motionValues.shakeY, motionValues.rotation])
// Death glitch
useEffect(() => {
if (state.type !== "death" || prefersReducedMotion) {
motionValues.glowIntensity.set(0)
return
}
let cancelled = false
let timeoutId: ReturnType<typeof setTimeout> | null = null
const scheduleIdle = (cb: () => void, delay: number) => {
timeoutId = setTimeout(() => {
// requestIdleCallback if available
const win = window as Window & { requestIdleCallback?: (cb: () => void) => number }
if (typeof win.requestIdleCallback === "function") {
win.requestIdleCallback(cb)
} else {
cb()
}
}, delay)
}
const glitchSequence = async () => {
const t = timing.glitch
const e = effects.glitch
// initial pulses
for (let i = 0; i < t.initialCount && !cancelled; i++) {
motionValues.contentScale.set(1 + Math.random() * e.scaleRange)
motionValues.glowIntensity.set(Math.random() * e.intensePulseMax)
await new Promise<void>(resolve => {
scheduleIdle(
resolve,
t.initialDelayMin + Math.random() * Math.max(0, t.initialDelayMax - t.initialDelayMin),
)
})
if (cancelled) break
motionValues.contentScale.set(1)
motionValues.glowIntensity.set(e.glowMax)
await new Promise<void>(resolve => {
scheduleIdle(resolve, t.pauseMin + Math.random() * Math.max(0, t.pauseMax - t.pauseMin))
})
}
// subtle loop (only one timeout pending at any time)
const subtle = () => {
if (cancelled) return
motionValues.glowIntensity.set(e.glowMin + Math.random() * (e.glowMax - e.glowMin))
scheduleIdle(
subtle,
t.subtleDelayMin + Math.random() * Math.max(0, t.subtleDelayMax - t.subtleDelayMin),
)
}
subtle()
}
glitchSequence()
return () => {
cancelled = true
if (timeoutId) clearTimeout(timeoutId)
motionValues.glowIntensity.set(0)
}
}, [state, prefersReducedMotion, motionValues.contentScale, motionValues.glowIntensity])
// Content scale pop on completion
useLayoutEffect(() => {
if (transition.justCompleted) {
motionValues.contentScale.set(0)
animate(motionValues.contentScale, [1.3, 1], springs.contentScale)
}
}, [transition.justCompleted, motionValues.contentScale])
}
================================================
FILE: src/components/feedback/DeathBubble.tsx
================================================
"use client"
import { animate, motion, useMotionValue, useTransform } from "motion/react"
import { useEffect, useMemo } from "react"
import { colors, shake, springs } from "@/animations"
import { dimensions } from "@/constants/dimensions"
interface DeathBubbleProps {
error: unknown
}
export function DeathBubble({ error }: DeathBubbleProps) {
// Generate glitchy characters instead of showing the real message
const glitchChars = useMemo(() => {
const source = error instanceof Error ? error.message : String(error ?? "DEAD")
const charset = "@#$%&*/=-+?!~<>\\|█▓▒░"
return Array.from({ length: Math.max(6, Math.min(source.length, 16)) })
.map(() => charset[Math.floor(Math.random() * charset.length)])
.join("")
}, [error])
const shakeX = useMotionValue(0)
const shakeY = useMotionValue(0)
const rotation = useMotionValue(0)
// Re-use the same shake behaviour as FailureBubble
useEffect(() => {
let cancelled = false
const shakeSequence = async () => {
if (cancelled) return
const shakeIntensity = shake.bubble.intensity
const shakeDuration = shake.bubble.duration
const shakeCount = shake.bubble.count
for (let i = 0; i < shakeCount; i++) {
if (cancelled) break
const xOffset = (Math.random() - 0.5) * shakeIntensity
const yOffset = (Math.random() - 0.5) * shakeIntensity + shake.bubble.yOffset
const rotOffset = (Math.random() - 0.5) * shake.bubble.rotationRange
await Promise.all([
animate(shakeX, xOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
animate(shakeY, yOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
animate(rotation, rotOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
])
if (cancelled) break
}
if (!cancelled) {
await Promise.all([
animate(shakeX, 0, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
animate(shakeY, shake.bubble.yOffset, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
animate(rotation, 0, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
])
}
}
const timer = setTimeout(shakeSequence, shake.bubble.delay)
return () => {
cancelled = true
clearTimeout(timer)
}
}, [shakeX, shakeY, rotation])
const background = "rgba(0,0,0,0.95)" // black background
const arrowColor = "rgba(0,0,0,0.95)"
return (
<motion.div
initial={{ opacity: 0, scale: 0.8, y: 20, filter: "blur(5px)" }}
animate={{ opacity: 1, scale: 1, y: -5, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.8, y: 20, filter: "blur(5px)" }}
transition={springs.failureBubble}
style={{
position: "absolute",
bottom: "100%",
left: "50%",
marginBottom: "8px",
x: useTransform([shakeX], ([x]) => `calc(-50% + ${x}px)`),
y: shakeY,
zIndex: 10,
rotate: rotation,
}}
>
<div
className="text-sm p-1 px-2 font-bold"
style={{
background,
color: "#dc2626", // red text
borderRadius: dimensions.failureBubble.borderRadius,
whiteSpace: "nowrap",
maxWidth: dimensions.failureBubble.maxWidth,
boxShadow: colors.failureBubble.shadow,
border: "1px solid rgba(220,38,38,0.9)",
}}
>
{glitchChars}
</div>
<div
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
width: 0,
height: 0,
borderLeft: `${dimensions.failureBubble.arrowSize} solid transparent`,
borderRight: `${dimensions.failureBubble.arrowSize} solid transparent`,
borderTop: `${dimensions.failureBubble.arrowSize} solid ${arrowColor}`,
}}
/>
</motion.div>
)
}
================================================
FILE: src/components/feedback/EffectLogo.tsx
================================================
import { motion, type Transition } from "motion/react"
export function EffectLogo({ className = "h-7" }: { className?: string }) {
// Base animation keyframes for the pulsing / glowing effect
const pulseKeyframes = {
scale: [1],
opacity: [1],
y: [0], // subtle vertical motion (up then back)
}
const baseTransition: Transition = {
duration: 2,
ease: "easeInOut" as const,
repeat: Infinity,
}
return (
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
style={{ overflow: "visible" }}
>
{/* Layer 1 */}
<motion.path
fillRule="evenodd"
clipRule="evenodd"
d="M29.8022 24.317C30.2747 24.05 30.4361 23.4582 30.1636 22.9953C29.891 22.5329 29.2873 22.3741 28.8148 22.6411L15.9211 29.9362L3.07463 22.6683C2.60281 22.4012 1.999 22.5594 1.72597 23.0225C1.45347 23.4854 1.61541 24.077 2.08741 24.3441L15.3897 31.8698C15.5053 31.9353 15.6327 31.9771 15.7645 31.9929C15.8963 32.0087 16.0299 31.9981 16.1576 31.9617C16.278 31.9433 16.3941 31.9031 16.5002 31.8431L29.8022 24.317Z"
fill="white"
style={{
transformOrigin: "50% 50%",
filter: "drop-shadow(0 0 4px rgba(59,130,246,0.4))",
}}
animate={pulseKeyframes}
transition={{ ...baseTransition, delay: 0 }}
/>
{/* Layer 2 */}
<motion.path
fillRule="evenodd"
clipRule="evenodd"
d="M31.1298 16.6012C31.1974 16.1929 31.0061 15.7675 30.6177 15.5488L16.555 7.63105C16.4443 7.56873 16.3234 7.52682 16.198 7.50732C16.0631 7.46888 15.922 7.45758 15.7827 7.47405C15.6434 7.49053 15.5088 7.53446 15.3865 7.60332L1.32289 15.5214C0.913972 15.7518 0.723686 16.2117 0.824499 16.6391C0.780205 16.9913 0.91787 17.3598 1.32768 17.5916L15.3904 25.5478C15.5127 25.6169 15.6475 25.661 15.7869 25.6776C15.9263 25.6942 16.0675 25.6829 16.2026 25.6445C16.3297 25.6253 16.4522 25.583 16.5642 25.5197L30.6275 17.563C31.0408 17.329 31.1776 16.9562 31.1298 16.6012ZM28.2266 16.5591L15.9459 9.64453L3.67206 16.5554L15.9528 23.5034L28.2266 16.5591Z"
fill="white"
style={{
transformOrigin: "50% 50%",
filter: "drop-shadow(0 0 4px rgba(59,130,246,0.4))",
}}
animate={pulseKeyframes}
transition={{ ...baseTransition, delay: 0.1 }}
/>
{/* Top filled + outline group */}
<motion.g
animate={pulseKeyframes}
transition={{ ...baseTransition, delay: 0.2 }}
style={{ transformOrigin: "50% 50%" }}
>
{/* Filled top shape */}
<path
fillRule="evenodd"
clipRule="evenodd"
d="M31.3429 10.6097C31.8677 10.3131 32.0476 9.65608 31.7442 9.14178C31.4416 8.62819 30.7712 8.45201 30.2464 8.74854L15.9269 16.8501L1.66063 8.77876C1.13584 8.48152 0.465408 8.65787 0.162793 9.172C-0.14053 9.68541 0.0391253 10.3432 0.564095 10.6397L15.337 18.9976C15.4654 19.0702 15.607 19.1165 15.7534 19.1339C15.8998 19.1514 16.0482 19.1395 16.19 19.0991C16.3236 19.0791 16.4524 19.0347 16.5701 18.9681L31.3429 10.6097Z"
fill="white"
style={{
filter: "drop-shadow(0 0 4px rgba(59,130,246,0.4))",
}}
/>
{/* Outline top shape */}
<path
fillRule="evenodd"
clipRule="evenodd"
d="M31.3255 8.49027C31.8513 8.78627 32.0333 9.44279 31.7325 9.95692C31.4307 10.4707 30.7603 10.6474 30.2344 10.3514L15.9128 2.28787L1.64317 10.3224C1.11731 10.6184 0.44688 10.4415 0.145328 9.92794C-0.15587 9.41381 0.0262664 8.75729 0.55159 8.46129L15.325 0.143725C15.4534 0.0713274 15.5949 0.0251207 15.7412 0.00776107C15.8875 -0.00959854 16.0358 0.00223134 16.1775 0.0425706C16.3093 0.0631109 16.4364 0.107185 16.5526 0.172702L31.3255 8.49027Z"
fill="white"
style={{
filter: "drop-shadow(0 0 4px rgba(59,130,246,0.4))",
}}
/>
<path
d="M2.7403 9.6795L15.8991 1.62024L29.0577 9.67879L15.8989 17.2013L2.7403 9.6795Z"
fill="white"
style={{
transformOrigin: "50% 50%",
filter: "drop-shadow(0 0 4px rgba(59,130,246,0.4))",
}}
/>
</motion.g>
{/* (Former Layer 5) is now part of the grouped animation above */}
</svg>
)
}
================================================
FILE: src/components/feedback/FailureBubble.tsx
================================================
"use client"
import { animate, motion, useMotionValue, useTransform } from "motion/react"
import { useEffect } from "react"
import { colors, shake, springs } from "@/animations"
import { dimensions } from "@/constants/dimensions"
interface FailureBubbleProps {
error: unknown
}
export function FailureBubble({ error }: FailureBubbleProps) {
const errorMessage = error instanceof Error ? error.message : String(error)
const shakeX = useMotionValue(0)
const shakeY = useMotionValue(0)
const rotation = useMotionValue(0)
// Shake animation when bubble appears
useEffect(() => {
let cancelled = false
const shakeSequence = async () => {
if (cancelled) return
// Shake animation - similar to error node but gentler
const shakeIntensity = shake.bubble.intensity
const shakeDuration = shake.bubble.duration
const shakeCount = shake.bubble.count
for (let i = 0; i < shakeCount; i++) {
if (cancelled) break
const xOffset = (Math.random() - 0.5) * shakeIntensity
const yOffset = (Math.random() - 0.5) * shakeIntensity + shake.bubble.yOffset
const rotOffset = (Math.random() - 0.5) * shake.bubble.rotationRange
await Promise.all([
animate(shakeX, xOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
animate(shakeY, yOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
animate(rotation, rotOffset, {
duration: shakeDuration,
ease: "easeInOut",
}).finished,
])
if (cancelled) break
}
// Return to rest position
if (!cancelled) {
await Promise.all([
animate(shakeX, 0, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
animate(shakeY, shake.bubble.yOffset, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
animate(rotation, 0, {
duration: shake.bubble.returnDuration,
ease: "easeOut",
}).finished,
])
}
}
// Start shake after a brief delay to let the bubble appear first
const timer = setTimeout(shakeSequence, shake.bubble.delay)
return () => {
cancelled = true
clearTimeout(timer)
}
}, [shakeX, shakeY, rotation])
return (
<motion.div
initial={{
opacity: 0,
scale: 0.8,
y: 20,
filter: "blur(5px)",
}}
animate={{
opacity: 1,
scale: 1,
y: -5,
filter: "blur(0px)",
}}
exit={{
opacity: 0,
scale: 0.8,
y: 20,
filter: "blur(5px)",
}}
transition={springs.failureBubble}
style={{
position: "absolute",
bottom: "100%",
left: "50%",
marginBottom: "8px",
x: useTransform([shakeX], ([x]) => `calc(-50% + ${x}px)`),
y: shakeY,
zIndex: 10,
rotate: rotation,
}}
>
<div
className={`text-sm p-1 px-2 ${colors.failureBubble.text} font-bold`}
style={{
background: colors.failureBubble.background,
borderRadius: dimensions.failureBubble.borderRadius,
whiteSpace: "nowrap",
maxWidth: dimensions.failureBubble.maxWidth,
boxShadow: colors.failureBubble.shadow,
}}
>
{errorMessage}
</div>
{/* Arrow pointing down */}
<div
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
width: 0,
height: 0,
borderLeft: `${dimensions.failureBubble.arrowSize} solid transparent`,
borderRight: `${dimensions.failureBubble.arrowSize} solid transparent`,
borderTop: `${dimensions.failureBubble.arrowSize} solid ${colors.failureBubble.background}`,
}}
/>
</motion.div>
)
}
================================================
FILE: src/components/feedback/FloatingHighlight.tsx
================================================
import { animate, motion, useMotionValue, useSpring, useTransform } from "motion/react"
import { useEffect, useRef } from "react"
interface HighlightTarget {
text: string
}
interface FloatingHighlightProps {
containerRef: React.RefObject<HTMLDivElement>
target: HighlightTarget | null
}
export const FloatingHighlight: React.FC<FloatingHighlightProps> = ({ containerRef, target }) => {
// Snappy spring animations for position and size
const springConfig = { bounce: 0.0, visualDuration: 0.2 }
const x = useSpring(0, springConfig)
const y = useSpring(0, springConfig)
const width = useSpring(0, springConfig)
const height = useSpring(0, springConfig)
const opacity = useMotionValue(0)
const scale = useSpring(1, { stiffness: 300, damping: 20 })
const highlightRef = useRef<HTMLDivElement>(null)
// Map opacity to blur: 0 opacity = 4px blur, 1 opacity = 0px blur
const blur = useTransform(opacity, [0, 1], [4, 0])
useEffect(() => {
if (!containerRef.current || !target) {
animate(opacity, 0, { ease: "linear" })
scale.set(0.9)
return
}
// Use requestAnimationFrame to ensure DOM is ready
const rafId = requestAnimationFrame(() => {
if (!containerRef.current) return
// Get the pre element inside the container
const preElement = containerRef.current.querySelector("pre")
if (!preElement) {
animate(opacity, 0, { ease: "linear" })
scale.set(0.95)
return
}
// Get all text content and build a map of character positions
const textNodes: Array<{ node: Node; start: number; text: string }> = []
let totalLength = 0
// Walk through all text nodes in the pre element
const walker = document.createTreeWalker(preElement, NodeFilter.SHOW_TEXT, null)
let node: Node | null
// biome-ignore lint/suspicious/noAssignInExpressions: because I don't care.
while ((node = walker.nextNode())) {
const text = node.textContent || ""
textNodes.push({ node, start: totalLength, text })
totalLength += text.length
}
// Get the full text content
const fullText = textNodes.map(n => n.text).join("")
// Find the target text in the full content
const targetIndex = fullText.indexOf(target.text)
if (targetIndex === -1) {
animate(opacity, 0, { ease: "linear" })
scale.set(0.95)
return
}
// Find which nodes contain the start and end of the target
const targetEnd = targetIndex + target.text.length
let startNode: Node | null = null
let startOffset = 0
let endNode: Node | null = null
let endOffset = 0
for (const { node, start, text } of textNodes) {
const nodeEnd = start + text.length
// Check if target starts in this node
if (!startNode && targetIndex >= start && targetIndex < nodeEnd) {
startNode = node
startOffset = targetIndex - start
}
// Check if target ends in this node
if (!endNode && targetEnd > start && targetEnd <= nodeEnd) {
endNode = node
endOffset = targetEnd - start
}
// If we found both, we can stop
if (startNode && endNode) break
}
if (startNode && endNode) {
try {
const range = document.createRange()
range.setStart(startNode, startOffset)
range.setEnd(endNode, endOffset)
// Get all client rects (in case text spans multiple lines)
const rects = Array.from(range.getClientRects())
if (rects.length > 0) {
const firstRect = rects[0]
if (!firstRect) return
// Calculate bounding box
const bounds = rects.reduce(
(acc, rect) => ({
left: Math.min(acc.left, rect.left),
top: Math.min(acc.top, rect.top),
right: Math.max(acc.right, rect.right),
bottom: Math.max(acc.bottom, rect.bottom),
}),
{
left: firstRect.left,
top: firstRect.top,
right: firstRect.right,
bottom: firstRect.bottom,
},
)
// Get container position
const containerRect = containerRef.current.getBoundingClientRect()
// Calculate relative position
const relX = bounds.left - containerRect.left
const relY = bounds.top - containerRect.top
const relWidth = bounds.right - bounds.left
const relHeight = bounds.bottom - bounds.top
// Apply with padding
const paddingX = 8
const paddingY = 6
x.set(relX - paddingX)
y.set(relY - paddingY)
width.set(relWidth + paddingX * 2)
height.set(relHeight + paddingY * 2)
animate(opacity, 1, { ease: "linear" })
scale.set(1)
}
} catch (e) {
console.error("Error creating range:", e)
animate(opacity, 0, { ease: "linear" })
scale.set(0.95)
}
} else {
animate(opacity, 0, { ease: "linear" })
scale.set(0.95)
}
})
return () => cancelAnimationFrame(rafId)
}, [target, containerRef, x, y, width, height, opacity, scale])
return (
<motion.div
ref={highlightRef}
style={{
position: "absolute",
left: 0,
top: 0,
x,
y,
width,
height,
opacity,
scale,
borderRadius: 6,
background: "rgba(56, 189, 248, 0.15)",
border: `1px solid rgba(56, 189, 248, 0.6)`,
boxShadow: `0 0 10px rgba(56, 189, 248, 0.3)`,
pointerEvents: "none",
zIndex: 10,
filter: useTransform(blur, v => `blur(${Math.max(0, v)}px)`),
}}
/>
)
}
================================================
FILE: src/components/feedback/NotificationBubble.tsx
================================================
"use client"
import { animate, motion, useMotionValue, useTransform } from "motion/react"
import { useEffect } from "react"
import { taskSounds } from "@/sounds/TaskSounds"
import type { Notification } from "@/VisualEffect"
interface NotificationBubbleProps {
notification: Notification
}
export function NotificationBubble({ notification }: NotificationBubbleProps) {
const floatY = useMotionValue(0)
// Play notification chime when notification appears
useEffect(() => {
taskSounds.playNotificationChime()
}, []) // Only play once when component mounts
// Gentle floating animation
useEffect(() => {
let cancelled = false
const floatSequence = async () => {
if (cancelled) return
// Faster floating up and down with ease in out
while (!cancelled) {
await animate(floatY, -12, {
duration: 0.8,
ease: "easeInOut",
}).finished
if (cancelled) break
await animate(floatY, 0, {
duration: 0.8,
ease: "easeInOut",
}).finished
}
}
floatSequence()
return () => {
cancelled = true
}
}, [floatY])
const transform = useTransform([floatY], ([y]) => `translateY(${y}px)`)
return (
<motion.div
style={{
position: "absolute",
bottom: "100%",
left: "50%",
x: "-50%",
zIndex: 1000,
pointerEvents: "none",
}}
initial={{ opacity: 0, scale: 0, y: 50, filter: "blur(10px)" }}
animate={{ opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0, y: 50, filter: "blur(10px)" }}
transition={{
type: "spring",
visualDuration: 0.3,
bounce: 0.1,
}}
>
<motion.div
style={{
transform,
}}
>
{/* Arrow pointing down */}
<div
style={{
position: "absolute",
bottom: -7,
left: "50%",
transform: "translateX(-50%)",
width: 0,
height: 0,
borderLeft: "8px solid transparent",
borderRight: "8px solid transparent",
borderTop: "8px solid #3b82f6", // Blue background
}}
/>
{/* Notification content */}
<div
className="text-xl p-2 px-3 text-white font-medium flex items-center gap-2"
style={{
background: "#3b82f6", // Blue background
borderRadius: "8px",
maxWidth: "200px",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(59, 130, 246, 0.3)",
}}
>
{notification.icon && <span className="text-lg">{notification.icon}</span>}
<span>{notification.message}</span>
</div>
</motion.div>
</motion.div>
)
}
================================================
FILE: src/components/feedback/index.ts
================================================
export { DeathBubble } from "./DeathBubble"
export { EffectLogo } from "./EffectLogo"
export { FailureBubble } from "./FailureBubble"
export { FloatingHighlight } from "./FloatingHighlight"
export { NotificationBubble } from "./NotificationBubble"
================================================
FILE: src/components/index.ts
================================================
// Content components
export * from "./CodeBlock"
// Display components
export * from "./display"
// Effect visualization
export * from "./effect"
// Feedback components
export * from "./feedback"
export * from "./HeaderView"
// Renderers
export * from "./renderers"
// UI components
export * from "./ui"
================================================
FILE: src/components/layout/NavigationSidebar.tsx
================================================
import { HashStraightIcon } from "@phosphor-icons/react"
import { memo, useMemo } from "react"
interface Example {
id: string
name: string
variant?: string
section?: string
}
interface NavigationSidebarProps {
examples: Array<Example>
currentExample?: string | undefined
onExampleSelect: (id: string) => void
}
function NavigationSidebarComponent({
currentExample,
examples,
onExampleSelect,
}: NavigationSidebarProps) {
// Group examples by section - memoize to avoid recomputation
const sections = useMemo(
() =>
examples.reduce(
(acc, example) => {
const section = example.section || "Other"
if (!acc[section]) {
acc[section] = []
}
acc[section].push(example)
return acc
},
{} as Record<string, Array<Example>>,
),
[examples],
)
return (
<aside
className="fixed top-0 w-76 h-screen z-40 hidden xl:block"
style={{ left: "max(0px, calc(50vw - 40rem))" }}
>
<div className="h-full overflow-y-auto border-r border-neutral-800 sidebar-scrollbar">
<div className="p-6">
<div className="mb-6">
<h2 className="text-sm font-mono text-neutral-500 mb-4">EXAMPLES</h2>
</div>
<div>
{Object.entries(sections).map(([sectionName, sectionExamples]) => (
<div key={sectionName} className="mb-8">
<h3 className="text-xs font-mono text-neutral-600 mb-3 font-bold tracking-wider">
<span className="flex items-center gap-1">
<HashStraightIcon size={14} />
{sectionName.toUpperCase()}
</span>
</h3>
<nav className="space-y-1">
{sectionExamples.map(example => {
const isActive = currentExample === example.id
return (
<button
type="button"
key={example.id}
onClick={() => onExampleSelect(example.id)}
className={`
w-full text-left py-1 px-2 -mx-2 text-sm font-mono cursor-pointer rounded-md
${
isActive
? "text-white"
: "text-neutral-400 hover:text-neutral-200 hover:bg-white/5"
}
focus:outline-none
`}
>
<span className="flex items-baseline gap-1.5">
<span>{example.name}</span>
{example.variant && (
<span className="text-xs text-neutral-500">{example.variant}</span>
)}
</span>
</button>
)
})}
</nav>
</div>
))}
</div>
</div>
</div>
</aside>
)
}
// Memoize the component to prevent re-renders when props don't change
export const NavigationSidebar = memo(NavigationSidebarComponent)
================================================
FILE: src/components/layout/PageHeader.tsx
================================================
import { StarFourIcon } from "@phosphor-icons/react"
import { motion, useAnimationFrame, useMotionValue, useTransform, useVelocity } from "motion/react"
import { useRouter } from "next/navigation"
import { useRef, useState } from "react"
import { VolumeToggle } from "@/components/ui"
interface HeaderProps {
isMuted: boolean
onMuteToggle: () => void
}
export function PageHeader({ isMuted, onMuteToggle }: HeaderProps) {
const [isHovering, setIsHovering] = useState(false)
const rotation = useMotionValue(0)
const velocity = useRef(770)
const lastTime = useRef(performance.now())
const router = useRouter()
// Use velocity hook to track rotation velocity
const rotationVelocity = useVelocity(rotation)
// Map velocity to opacity: 0 deg/s = 0.6 opacity, 1200 deg/s = 1.0 opacity
const opacity = useTransform(rotationVelocity, [0, 1200], [0.6, 1.0])
useAnimationFrame(time => {
const deltaTime = (time - lastTime.current) / 1000 // Convert to seconds
lastTime.current = time
const currentRotation = rotation.get()
if (isHovering) {
// Accelerate
velocity.current = Math.min(velocity.current + 800 * deltaTime, 1200) // Max 1200 deg/s
rotation.set(currentRotation + velocity.current * deltaTime)
} else if (velocity.current > 0) {
// Decelerate while still spinning
velocity.current = Math.max(velocity.current - 400 * deltaTime, 0)
rotation.set(currentRotation + velocity.current * deltaTime)
} else if (Math.abs(currentRotation % 360) > 0.1) {
// When stopped, smoothly return to neutral
const normalizedRotation = currentRotation % 360
const distanceToNeutral =
normalizedRotation > 180 ? 360 - normalizedRotation : -normalizedRotation
const returnSpeed = Math.min(Math.abs(distanceToNeutral) * 3, 200)
const direction = distanceToNeutral > 0 ? 1 : -1
rotation.set(currentRotation + direction * returnSpeed * deltaTime)
// Snap when very close
if (Math.abs(rotation.get() % 360) < 0.5) {
rotation.set(Math.round(rotation.get() / 360) * 360)
}
}
})
return (
<div className="self-start w-full flex items-start justify-between text-base">
{/* Left side: Logo and title */}
<button
type="button"
className="flex items-center gap-1.5 sm:gap-3 group select-none cursor-pointer"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onClick={() => {
// Use router.push for client-side navigation
router.push("/")
}}
>
<motion.div style={{ rotate: rotation, opacity }} className="flex items-center">
<StarFourIcon size={16} weight="fill" />
</motion.div>
<span className="font-bold text-neutral-400 tracking-wide text-base">VISUAL EFFECT</span>
</button>
{/* Right side: Sound controls and credit */}
<div className="flex items-end gap-1 flex-col">
{/* Sound control */}
<VolumeToggle isMuted={isMuted} onToggle={onMuteToggle} />
</div>
</div>
)
}
================================================
FILE: src/components/renderers/ArrayResult.tsx
================================================
"use client"
import { motion } from "motion/react"
import type { RenderableResult } from "./RenderableResult"
export class ArrayResult<T> implements RenderableResult {
constructor(public values: Array<T>) {}
render() {
return (
<motion.span
key="array"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ fontSize: 12 }}
>
[{this.values.length}]
</motion.span>
)
}
}
================================================
FILE: src/components/renderers/BasicRenderers.tsx
================================================
import type { RenderableResult } from "./RenderableResult"
// Simple number renderer
export class NumberResult implements RenderableResult {
constructor(public value: number) {}
render() {
return <div className="text-white font-mono text-xl">{this.value}</div>
}
}
// Simple string renderer
export class StringResult implements RenderableResult {
constructor(public value: string) {}
render() {
return <div className="text-white font-mono text-xl">{this.value}</div>
}
}
// Boolean renderer
export class BooleanResult implements RenderableResult {
constructor(public value: boolean) {}
render() {
return <div className="text-white font-mono">{this.value ? "true" : "false"}</div>
}
}
// Object/JSON renderer
export class ObjectResult implements RenderableResult {
constructor(public value: unknown) {}
render() {
return <div className="text-white font-mono text-xs">{JSON.stringify(this.value, null, 2)}</div>
}
}
================================================
FILE: src/components/renderers/EmojiResult.tsx
================================================
import type { RenderableResult } from "./RenderableResult"
export class EmojiResult implements RenderableResult {
constructor(public emoji: string) {}
render() {
return (
<span key="emoji" className="text-4xl">
{this.emoji}
</span>
)
}
}
================================================
FILE: src/components/renderers/RenderableResult.ts
================================================
import type { ReactNode } from "react"
// Base interface for renderable results
export interface RenderableResult {
render(): ReactNode
}
export interface RenderOptions {
dimensions: {
size: number
}
}
// Type guard to check if a result is renderable
export function isRenderableResult(value: unknown): value is RenderableResult {
return (
value !== null &&
typeof value === "object" &&
"render" in value &&
typeof (value as RenderableResult).render === "function"
)
}
// Helper function to render any result
export function renderResult(result: unknown): ReactNode {
if (isRenderableResult(result)) {
return result.render()
}
// Default string rendering for non-renderable results
return String(result)
}
================================================
FILE: src/components/renderers/TemperatureResult.tsx
================================================
import type { RenderableResult } from "./RenderableResult"
export class TemperatureResult implements RenderableResult {
constructor(
public value: number,
public location?: string,
) {}
render() {
return (
<div key="temp" className="text-xl">
{this.value}°
</div>
)
}
}
export class TemperatureArrayResult implements RenderableResult {
constructor(public values: Array<number>) {}
render() {
return (
<div key="array" className="text-xl">
[{this.values.map(t => `${t}°`).join(", ")}]
</div>
)
}
}
================================================
FILE: src/components/renderers/index.ts
================================================
export * from "./ArrayResult"
export * from "./BasicRenderers"
export * from "./EmojiResult"
export * from "./RenderableResult"
export * from "./TemperatureResult"
================================================
FILE: src/components/scope/FinalizerCard.tsx
================================================
import { AnimatePresence, motion } from "motion/react"
import { useEffect, useRef, useState } from "react"
import { springs } from "@/animations"
import type { Finalizer } from "../../VisualScope"
interface FinalizerCardProps {
finalizer: Finalizer
}
export function FinalizerCard({ finalizer }: FinalizerCardProps) {
const isRunning = finalizer.state === "running"
const isCompleted = finalizer.state === "completed"
// Track state transitions
const prevStateRef = useRef(finalizer.state)
const [justCompleted, setJustCompleted] = useState(false)
useEffect(() => {
if (prevStateRef.current !== "completed" && finalizer.state === "completed") {
setJustCompleted(true)
const timeout = setTimeout(() => setJustCompleted(false), 600) // Match animation duration
return () => clearTimeout(timeout)
}
prevStateRef.current = finalizer.state
}, [finalizer.state])
return (
<motion.div
initial={{
opacity: 0,
scale: 1.2,
filter: "blur(4px)",
}}
animate={{
opacity: 1,
scale: 1,
filter: "blur(0px)",
}}
exit={{
opacity: 0,
scale: 0.8,
filter: "blur(4px)",
}}
transition={{
type: "spring",
visualDuration: 0.3,
bounce: 0.3,
}}
className={`relative flex items-center gap-3 px-4 py-3 rounded-lg h-[52px] shadow-lg shadow-neutral-900 transition-colors duration-200 ${
finalizer.state === "pending"
? "bg-neutral-800 border border-neutral-700"
: isRunning
? "bg-blue-900 border border-blue-500 "
: "bg-green-900 border border-green-500"
}`}
style={{
minWidth: "200px",
willChange: "transform, opacity, filter",
translateZ: 0,
}}
>
{/* Checkbox container */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1,
gitextract_q3jrgzjr/ ├── .gitignore ├── CLAUDE.md ├── LICENSE ├── README.md ├── app/ │ ├── ClientAppContent.tsx │ ├── [exampleId]/ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── biome.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.mjs ├── public/ │ ├── _headers │ ├── generate-favicons.html │ ├── robots.txt │ ├── site.webmanifest │ └── sitemap.xml ├── scripts/ │ └── generate-og-images.tsx ├── src/ │ ├── AppContent.tsx │ ├── VisualEffect.test.ts │ ├── VisualEffect.ts │ ├── VisualRef.ts │ ├── VisualScope.ts │ ├── animations.ts │ ├── components/ │ │ ├── CodeBlock.tsx │ │ ├── HeaderView.tsx │ │ ├── ScheduleTimeline.tsx │ │ ├── Timer.tsx │ │ ├── display/ │ │ │ ├── EffectExample.tsx │ │ │ ├── RefDisplay.tsx │ │ │ └── index.ts │ │ ├── effect/ │ │ │ ├── EffectContainer.tsx │ │ │ ├── EffectContent.tsx │ │ │ ├── EffectLabel.tsx │ │ │ ├── EffectNode.tsx │ │ │ ├── EffectOverlay.tsx │ │ │ ├── index.ts │ │ │ ├── nodeVariants.ts │ │ │ ├── taskUtils.ts │ │ │ └── useEffectMotion.ts │ │ ├── feedback/ │ │ │ ├── DeathBubble.tsx │ │ │ ├── EffectLogo.tsx │ │ │ ├── FailureBubble.tsx │ │ │ ├── FloatingHighlight.tsx │ │ │ ├── NotificationBubble.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── layout/ │ │ │ ├── NavigationSidebar.tsx │ │ │ └── PageHeader.tsx │ │ ├── renderers/ │ │ │ ├── ArrayResult.tsx │ │ │ ├── BasicRenderers.tsx │ │ │ ├── EmojiResult.tsx │ │ │ ├── RenderableResult.ts │ │ │ ├── TemperatureResult.tsx │ │ │ └── index.ts │ │ ├── scope/ │ │ │ ├── FinalizerCard.tsx │ │ │ ├── ScopeStack.tsx │ │ │ └── utils.ts │ │ └── ui/ │ │ ├── QuickOpen.tsx │ │ ├── SegmentedControl.tsx │ │ ├── VolumeToggle.tsx │ │ └── index.ts │ ├── constants/ │ │ ├── colors.ts │ │ └── dimensions.ts │ ├── examples/ │ │ ├── effect-acquire-release.tsx │ │ ├── effect-add-finalizer.tsx │ │ ├── effect-all-short-circuit.tsx │ │ ├── effect-all.tsx │ │ ├── effect-die.tsx │ │ ├── effect-eventually.tsx │ │ ├── effect-fail.tsx │ │ ├── effect-firstsuccessof.tsx │ │ ├── effect-foreach.tsx │ │ ├── effect-orelse.tsx │ │ ├── effect-partition.tsx │ │ ├── effect-promise.tsx │ │ ├── effect-race.tsx │ │ ├── effect-raceall.tsx │ │ ├── effect-repeat-spaced.tsx │ │ ├── effect-repeat-while-output.tsx │ │ ├── effect-retry-exponential.tsx │ │ ├── effect-retry-recurs.tsx │ │ ├── effect-sleep.tsx │ │ ├── effect-succeed.tsx │ │ ├── effect-sync.tsx │ │ ├── effect-timeout.tsx │ │ ├── effect-validate.tsx │ │ ├── helpers.ts │ │ ├── ref-make.tsx │ │ └── ref-update-and-get.tsx │ ├── hooks/ │ │ ├── useOptionKey.ts │ │ ├── useStateTransition.ts │ │ ├── useVisualEffects.ts │ │ └── useVisualScope.ts │ ├── lib/ │ │ ├── example-types.ts │ │ └── examples-manifest.ts │ ├── shared/ │ │ ├── appItems.ts │ │ └── idUtils.ts │ ├── sounds/ │ │ └── TaskSounds.ts │ └── theme.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.scripts.json ├── vitest.config.ts └── wrangler.jsonc
Condensed preview — 109 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (338K chars).
[
{
"path": ".gitignore",
"chars": 353,
"preview": "# Logs\nlogs\n*.log\n.next\nout/\n.vscode\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nno..."
},
{
"path": "CLAUDE.md",
"chars": 12647,
"preview": "# Visual Effect - Codebase Documentation\n\n## Overview\n\nVisual Effect is an interactive visualization tool for the Effect..."
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2025 Kit Langton\n\nPermission is hereby granted, free of charge, to any person obtaining a cop..."
},
{
"path": "README.md",
"chars": 426,
"preview": "# Visual Effect\n\nInteractive visualizations for [Effect](https://github.com/Effect-TS/effect) programs, built with Effec..."
},
{
"path": "app/ClientAppContent.tsx",
"chars": 257,
"preview": "\"use client\"\n\nimport dynamic from \"next/dynamic\"\n\nconst AppContent = dynamic(\n () => import(\"../src/AppContent\").then(m..."
},
{
"path": "app/[exampleId]/page.tsx",
"chars": 1810,
"preview": "/* eslint-disable react-refresh/only-export-components */\nimport type { Metadata } from \"next\"\nimport { examplesManifest..."
},
{
"path": "app/globals.css",
"chars": 841,
"preview": "@import \"tailwindcss\";\n\n/* Dark mode scrollbar for all elements */\n* {\n scrollbar-width: thin;\n scrollbar-color: #2626..."
},
{
"path": "app/layout.tsx",
"chars": 1838,
"preview": "/* eslint-disable react-refresh/only-export-components */\nimport \"./globals.css\"\nimport ClientAppContent from \"./ClientA..."
},
{
"path": "app/page.tsx",
"chars": 53,
"preview": "export default function HomePage() {\n return null\n}\n"
},
{
"path": "biome.json",
"chars": 1452,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.2.5/schema.json\",\n \"assist\": { \"actions\": { \"source\": { \"organizeImports\"..."
},
{
"path": "next-env.d.ts",
"chars": 260,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference path=\"./out/types/routes..."
},
{
"path": "next.config.js",
"chars": 678,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n turbopack: {},\n output: \"export\",\n trailingSlash: true..."
},
{
"path": "package.json",
"chars": 2471,
"preview": "{\n \"name\": \"visual-effect\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"sideEffects\": false,\n \"lic..."
},
{
"path": "postcss.config.mjs",
"chars": 69,
"preview": "export default {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n}\n"
},
{
"path": "public/_headers",
"chars": 302,
"preview": "/*\n X-Content-Type-Options: nosniff\n X-Frame-Options: DENY\n X-XSS-Protection: 1; mode=block\n\n/_next/static/*\n Cache-..."
},
{
"path": "public/generate-favicons.html",
"chars": 1168,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <title>Generate Favicons</title>\n</head>\n<body>\n <h1>Generate PNG Favicons from SVG..."
},
{
"path": "public/robots.txt",
"chars": 125,
"preview": "# robots.txt for Visual Effect\n\nUser-agent: *\nAllow: /\n\n# Sitemap location\nSitemap: https://effect.kitlangton.com/sitema..."
},
{
"path": "public/site.webmanifest",
"chars": 468,
"preview": "{\n \"name\": \"Visual Effect - Interactive Effect Library Visualizer\",\n \"short_name\": \"Visual Effect\",\n \"description\": \"..."
},
{
"path": "public/sitemap.xml",
"chars": 271,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n <url>\n <loc>htt..."
},
{
"path": "scripts/generate-og-images.tsx",
"chars": 6457,
"preview": "/* eslint-disable react-refresh/only-export-components */\n\nimport fs from \"node:fs/promises\"\nimport path from \"node:path..."
},
{
"path": "src/AppContent.tsx",
"chars": 11495,
"preview": "\"use client\"\n\nimport {\n ArrowClockwiseIcon,\n HashStraightIcon,\n HeartIcon,\n PlayIcon,\n SkullIcon,\n StopIcon,\n} fro..."
},
{
"path": "src/VisualEffect.test.ts",
"chars": 9250,
"preview": "import { Effect } from \"effect\"\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\"\nimport { Visual..."
},
{
"path": "src/VisualEffect.ts",
"chars": 11513,
"preview": "\"use client\"\n\nimport { Context, Effect, Fiber, Option } from \"effect\"\nimport { useSyncExternalStore } from \"react\"\nimpor..."
},
{
"path": "src/VisualRef.ts",
"chars": 3177,
"preview": "\"use client\"\n\nimport { Effect, Ref } from \"effect\"\nimport { useMemo, useSyncExternalStore } from \"react\"\nimport { taskSo..."
},
{
"path": "src/VisualScope.ts",
"chars": 2177,
"preview": "import { taskSounds } from \"./sounds/TaskSounds\"\n\nexport type ScopeState = \"idle\" | \"acquiring\" | \"active\" | \"releasing\"..."
},
{
"path": "src/animations.ts",
"chars": 3310,
"preview": "// Animation configuration for consistent motion design across components\n\n// Default spring for MotionConfig wrapper\nex..."
},
{
"path": "src/components/CodeBlock.tsx",
"chars": 6384,
"preview": "import type { MotionStyle } from \"motion/react\"\nimport { AnimatePresence, motion } from \"motion/react\"\nimport type { Lan..."
},
{
"path": "src/components/HeaderView.tsx",
"chars": 10437,
"preview": "\"use client\"\nimport {\n ArrowCounterClockwiseIcon,\n CheckIcon,\n LinkIcon,\n PlayIcon,\n StarFourIcon,\n StopIcon,\n} fr..."
},
{
"path": "src/components/ScheduleTimeline.tsx",
"chars": 20576,
"preview": "import { motion } from \"motion/react\"\nimport React, { useEffect, useRef, useState } from \"react\"\nimport type { VisualEff..."
},
{
"path": "src/components/Timer.tsx",
"chars": 1690,
"preview": "import { useEffect, useRef, useState } from \"react\"\nimport type { VisualEffect } from \"@/VisualEffect\"\n\nfunction useTime..."
},
{
"path": "src/components/display/EffectExample.tsx",
"chars": 10557,
"preview": "\"use client\"\n\nimport { ArrowRightIcon } from \"@phosphor-icons/react\"\nimport { motion } from \"motion/react\"\nimport { memo..."
},
{
"path": "src/components/display/RefDisplay.tsx",
"chars": 2528,
"preview": "import { AnimatePresence, motion, type Transition } from \"motion/react\"\nimport { useVisualRef, type VisualRef } from \"@/..."
},
{
"path": "src/components/display/index.ts",
"chars": 145,
"preview": "export { ScheduleTimeline } from \"../ScheduleTimeline\"\nexport { EffectExample } from \"./EffectExample\"\nexport { RefDispl..."
},
{
"path": "src/components/effect/EffectContainer.tsx",
"chars": 3004,
"preview": "import { motion, useTransform } from \"motion/react\"\nimport { colors, effects } from \"@/animations\"\nimport type { VisualE..."
},
{
"path": "src/components/effect/EffectContent.tsx",
"chars": 3906,
"preview": "import { SkullIcon, StarFourIcon, WarningOctagonIcon } from \"@phosphor-icons/react\"\nimport { AnimatePresence, motion } f..."
},
{
"path": "src/components/effect/EffectLabel.tsx",
"chars": 825,
"preview": "import { motion } from \"motion/react\"\nimport { Timer } from \"@/components/Timer\"\nimport { theme } from \"@/theme\"\nimport..."
},
{
"path": "src/components/effect/EffectNode.tsx",
"chars": 3254,
"preview": "import { AnimatePresence, motion } from \"motion/react\"\nimport { memo, useCallback, useState } from \"react\"\nimport {\n us..."
},
{
"path": "src/components/effect/EffectOverlay.tsx",
"chars": 2089,
"preview": "import { motion } from \"motion/react\"\nimport { theme } from \"../../theme\"\nimport type { EffectMotionValues } from \"./use..."
},
{
"path": "src/components/effect/index.ts",
"chars": 491,
"preview": "export { EffectContainer } from \"./EffectContainer\"\nexport { EffectContent } from \"./EffectContent\"\nexport { EffectLabel..."
},
{
"path": "src/components/effect/nodeVariants.ts",
"chars": 1974,
"preview": "import { springs } from \"@/animations\"\nimport { TASK_COLORS } from \"../../constants/colors\"\n\n// Hybrid approach: Only ha..."
},
{
"path": "src/components/effect/taskUtils.ts",
"chars": 390,
"preview": "import { SHADOW_COLORS } from \"../../constants/colors\"\nimport { theme } from \"../../theme\"\nimport type { VisualEffect }..."
},
{
"path": "src/components/effect/useEffectMotion.ts",
"chars": 13034,
"preview": "import {\n type AnimationPlaybackControls,\n animate,\n type MotionValue,\n useMotionValue,\n useSpring,\n useTransform,..."
},
{
"path": "src/components/feedback/DeathBubble.tsx",
"chars": 4191,
"preview": "\"use client\"\n\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\"\nimport { useEffect, useMemo }..."
},
{
"path": "src/components/feedback/EffectLogo.tsx",
"chars": 4313,
"preview": "import { motion, type Transition } from \"motion/react\"\n\nexport function EffectLogo({ className = \"h-7\" }: { className?:..."
},
{
"path": "src/components/feedback/FailureBubble.tsx",
"chars": 4048,
"preview": "\"use client\"\n\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\"\nimport { useEffect } from \"re..."
},
{
"path": "src/components/feedback/FloatingHighlight.tsx",
"chars": 5898,
"preview": "import { animate, motion, useMotionValue, useSpring, useTransform } from \"motion/react\"\nimport { useEffect, useRef } fro..."
},
{
"path": "src/components/feedback/NotificationBubble.tsx",
"chars": 2819,
"preview": "\"use client\"\n\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\"\nimport { useEffect } from \"re..."
},
{
"path": "src/components/feedback/index.ts",
"chars": 248,
"preview": "export { DeathBubble } from \"./DeathBubble\"\nexport { EffectLogo } from \"./EffectLogo\"\nexport { FailureBubble } from \"./F..."
},
{
"path": "src/components/index.ts",
"chars": 305,
"preview": "// Content components\nexport * from \"./CodeBlock\"\n// Display components\nexport * from \"./display\"\n// Effect visualizatio..."
},
{
"path": "src/components/layout/NavigationSidebar.tsx",
"chars": 3234,
"preview": "import { HashStraightIcon } from \"@phosphor-icons/react\"\nimport { memo, useMemo } from \"react\"\n\ninterface Example {\n id..."
},
{
"path": "src/components/layout/PageHeader.tsx",
"chars": 3112,
"preview": "import { StarFourIcon } from \"@phosphor-icons/react\"\nimport { motion, useAnimationFrame, useMotionValue, useTransform, u..."
},
{
"path": "src/components/renderers/ArrayResult.tsx",
"chars": 479,
"preview": "\"use client\"\n\nimport { motion } from \"motion/react\"\nimport type { RenderableResult } from \"./RenderableResult\"\n\nexport c..."
},
{
"path": "src/components/renderers/BasicRenderers.tsx",
"chars": 962,
"preview": "import type { RenderableResult } from \"./RenderableResult\"\n\n// Simple number renderer\nexport class NumberResult implemen..."
},
{
"path": "src/components/renderers/EmojiResult.tsx",
"chars": 274,
"preview": "import type { RenderableResult } from \"./RenderableResult\"\n\nexport class EmojiResult implements RenderableResult {\n con..."
},
{
"path": "src/components/renderers/RenderableResult.ts",
"chars": 753,
"preview": "import type { ReactNode } from \"react\"\n\n// Base interface for renderable results\nexport interface RenderableResult {\n r..."
},
{
"path": "src/components/renderers/TemperatureResult.tsx",
"chars": 579,
"preview": "import type { RenderableResult } from \"./RenderableResult\"\n\nexport class TemperatureResult implements RenderableResult {..."
},
{
"path": "src/components/renderers/index.ts",
"chars": 164,
"preview": "export * from \"./ArrayResult\"\nexport * from \"./BasicRenderers\"\nexport * from \"./EmojiResult\"\nexport * from \"./Renderable..."
},
{
"path": "src/components/scope/FinalizerCard.tsx",
"chars": 5504,
"preview": "import { AnimatePresence, motion } from \"motion/react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { spri..."
},
{
"path": "src/components/scope/ScopeStack.tsx",
"chars": 6549,
"preview": "import { CaretRightIcon } from \"@phosphor-icons/react\"\nimport { AnimatePresence, motion } from \"motion/react\"\nimport { u..."
},
{
"path": "src/components/scope/utils.ts",
"chars": 671,
"preview": "import type { VisualScope } from \"../../VisualScope\"\n\nexport function isActive(state: VisualScope[\"state\"]) {\n return s..."
},
{
"path": "src/components/ui/QuickOpen.tsx",
"chars": 9387,
"preview": "import type React from \"react\"\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\ninterface QuickOpenItem..."
},
{
"path": "src/components/ui/SegmentedControl.tsx",
"chars": 2747,
"preview": "import { motion } from \"motion/react\"\nimport { useEffect, useRef, useState } from \"react\"\n\ninterface SegmentedControlPro..."
},
{
"path": "src/components/ui/VolumeToggle.tsx",
"chars": 3786,
"preview": "import { AnimatePresence, motion, useSpring } from \"motion/react\"\nimport { useEffect, useRef } from \"react\"\nimport { tas..."
},
{
"path": "src/components/ui/index.ts",
"chars": 140,
"preview": "export { QuickOpen } from \"./QuickOpen\"\nexport { SegmentedControl } from \"./SegmentedControl\"\nexport { VolumeToggle } fr..."
},
{
"path": "src/constants/colors.ts",
"chars": 513,
"preview": "// Centralized color constants for task states\nexport const TASK_COLORS = {\n idle: \"var(--color-slate-600)\",\n running:..."
},
{
"path": "src/constants/dimensions.ts",
"chars": 359,
"preview": "// Layout and dimensional constants\nexport const dimensions = {\n // Effect node dimensions\n node: {\n width: 64,..."
},
{
"path": "src/examples/effect-acquire-release.tsx",
"chars": 5321,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useEffect, useMemo, useRef } from \"react\"\nimport { EffectExample..."
},
{
"path": "src/examples/effect-add-finalizer.tsx",
"chars": 5290,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { memo, useEffect, useMemo, useState } from \"react\"\nimport { Effect..."
},
{
"path": "src/examples/effect-all-short-circuit.tsx",
"chars": 2887,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo, useRef } from \"react\"\nimport { EffectExample } from \"@/c..."
},
{
"path": "src/examples/effect-all.tsx",
"chars": 5231,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { memo, useCallback, useMemo, useState } from \"react\"\nimport { Effe..."
},
{
"path": "src/examples/effect-die.tsx",
"chars": 1365,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-eventually.tsx",
"chars": 2464,
"preview": "\"use client\"\n\nimport { Effect, Schedule } from \"effect\"\nimport { useMemo, useRef } from \"react\"\nimport { EffectExample }..."
},
{
"path": "src/examples/effect-fail.tsx",
"chars": 1034,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-firstsuccessof.tsx",
"chars": 2668,
"preview": "\"use client\";\n\nimport { Effect } from \"effect\";\nimport { useMemo } from \"react\";\nimport { EffectExample } from \"@/compon..."
},
{
"path": "src/examples/effect-foreach.tsx",
"chars": 2105,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-orelse.tsx",
"chars": 2579,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo, useRef } from \"react\"\nimport { EffectExample } from \"@/c..."
},
{
"path": "src/examples/effect-partition.tsx",
"chars": 2654,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-promise.tsx",
"chars": 1264,
"preview": "\"use client\"\n\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/components/display\"\nimport { useVisualEff..."
},
{
"path": "src/examples/effect-race.tsx",
"chars": 1914,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-raceall.tsx",
"chars": 2151,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-repeat-spaced.tsx",
"chars": 2738,
"preview": "\"use client\"\n\nimport { Effect, Schedule } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@..."
},
{
"path": "src/examples/effect-repeat-while-output.tsx",
"chars": 2642,
"preview": "\"use client\"\n\nimport { Duration, Effect, Schedule } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample..."
},
{
"path": "src/examples/effect-retry-exponential.tsx",
"chars": 2235,
"preview": "\"use client\"\n\nimport { Effect, Schedule } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@..."
},
{
"path": "src/examples/effect-retry-recurs.tsx",
"chars": 2934,
"preview": "\"use client\"\n\nimport { Effect, Schedule } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@..."
},
{
"path": "src/examples/effect-sleep.tsx",
"chars": 1456,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-succeed.tsx",
"chars": 1139,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-sync.tsx",
"chars": 1169,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } from \"@/component..."
},
{
"path": "src/examples/effect-timeout.tsx",
"chars": 2021,
"preview": "\"use client\"\n\nimport { Effect } from \"effect\"\nimport { useMemo, useRef } from \"react\"\nimport { EffectExample } from \"@/c..."
},
{
"path": "src/examples/effect-validate.tsx",
"chars": 6481,
"preview": "\"use client\"\n\nimport { Cause, Effect } from \"effect\"\nimport { AnimatePresence, motion } from \"motion/react\"\nimport { use..."
},
{
"path": "src/examples/helpers.ts",
"chars": 2074,
"preview": "import { Effect } from \"effect\"\nimport { EmojiResult, TemperatureResult } from \"../components/renderers\"\n\n/**\n * Generat..."
},
{
"path": "src/examples/ref-make.tsx",
"chars": 2707,
"preview": "\"use client\"\n\nimport { Duration, Effect, Ref, Schedule } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectEx..."
},
{
"path": "src/examples/ref-update-and-get.tsx",
"chars": 4073,
"preview": "\"use client\"\n\nimport { Duration, Effect, Ref } from \"effect\"\nimport { useMemo } from \"react\"\nimport { EffectExample } fr..."
},
{
"path": "src/hooks/useOptionKey.ts",
"chars": 1862,
"preview": "import { useEffect, useState } from \"react\"\n\nlet globalOptionPressed = false\nconst subscribers: Set<(pressed: boolean) =..."
},
{
"path": "src/hooks/useStateTransition.ts",
"chars": 1016,
"preview": "import { useEffect, useRef } from \"react\"\nimport type { EffectState } from \"../VisualEffect\"\n\nexport interface StateTran..."
},
{
"path": "src/hooks/useVisualEffects.ts",
"chars": 1921,
"preview": "import type { Effect } from \"effect\"\nimport { type DependencyList, useMemo } from \"react\"\nimport { type VisualEffect, vi..."
},
{
"path": "src/hooks/useVisualScope.ts",
"chars": 295,
"preview": "import { useEffect, useReducer } from \"react\"\nimport type { VisualScope } from \"../VisualScope\"\n\nexport function useVisu..."
},
{
"path": "src/lib/example-types.ts",
"chars": 427,
"preview": "export interface ExampleMeta {\n id: string\n name: string\n variant?: string\n description: string\n section: \"construc..."
},
{
"path": "src/lib/examples-manifest.ts",
"chars": 5160,
"preview": "import type { ExampleMeta } from \"./example-types\"\n\n// This is the single source of truth for all examples\n// Examples a..."
},
{
"path": "src/shared/appItems.ts",
"chars": 298,
"preview": "import type { AppItem } from \"../lib/example-types\"\nimport { examplesManifest } from \"../lib/examples-manifest\"\nimport {..."
},
{
"path": "src/shared/idUtils.ts",
"chars": 373,
"preview": "function normalizeSegment(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n .toLow..."
},
{
"path": "src/sounds/TaskSounds.ts",
"chars": 14099,
"preview": "import * as Tone from \"tone\"\n\n// Musical scale configuration\nconst PENTATONIC_SCALE = [\"C\", \"D\", \"E\", \"G\", \"A\"] as const..."
},
{
"path": "src/theme.ts",
"chars": 305,
"preview": "// Design tokens - only the values actually used in the codebase\nexport const theme = {\n colors: {\n textPrimary: \"#f..."
},
{
"path": "tailwind.config.js",
"chars": 298,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./app/**/*.{js,ts,jsx,tsx,mdx}\", \"./src/**/*.{..."
},
{
"path": "tsconfig.app.json",
"chars": 1014,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2022\", \"DOM\", \"DOM...."
},
{
"path": "tsconfig.json",
"chars": 1061,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": false,..."
},
{
"path": "tsconfig.node.json",
"chars": 666,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",..."
},
{
"path": "tsconfig.scripts.json",
"chars": 418,
"preview": "{\n \"extends\": \"./tsconfig.app.json\",\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"target\": \"ES2022\",\n \"jsx\":..."
},
{
"path": "vitest.config.ts",
"chars": 139,
"preview": "import { defineConfig } from \"vitest/config\"\n\nexport default defineConfig({\n test: {\n environment: \"jsdom\",\n glob..."
},
{
"path": "wrangler.jsonc",
"chars": 401,
"preview": "{\n \"$schema\": \"./node_modules/wrangler/config-schema.json\",\n \"name\": \"visual-effect\",\n \"compatibility_date\": \"2026-07..."
}
]
About this extraction
This page contains the full source code of the kitlangton/visual-effect GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 109 files (309.2 KB), approximately 79.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.